Skip to content

HMesh Module

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.

Manifold

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.

Source code in pygel3d/hmesh.py
class Manifold:
    """ 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.
    """
    def __init__(self, orig: Self | ct.c_void_p | None = None):
        """ Construct a Manifold object. If orig is None, a new empty Manifold is created. If
        orig is a Manifold, a copy of it is created. If orig is a c_void_p, it is assumed to be
        a pointer to a Manifold object created in C++. In this case, the object is not copied,
        but the pointer is used directly. If orig is anything else, a TypeError is raised.
        """
        if orig == None:
            self.obj = lib_py_gel.Manifold_new()
        elif isinstance(orig, Manifold):
            self.obj = lib_py_gel.Manifold_copy(orig.obj)
        elif isinstance(orig, ct.c_void_p):
            self.obj = orig
        else:
            raise TypeError(f"Manifold constructor takes either a Manifold or a c_void_p as argument, not {type(orig)}")

    @classmethod
    def from_triangles(cls, vertices: ArrayLike, faces: ArrayLike) -> Self:
        """ Given a list of vertices and triangles (faces), this function produces
        a Manifold mesh."""
        m = cls()
        vertices = np.asarray(vertices,dtype=np.float64, order='C')
        faces = np.asarray(faces,dtype=ct.c_int, order='C')
        m.obj = lib_py_gel.Manifold_from_triangles(_n_vec3(vertices), _n_vec3(faces), vertices, faces)
        return m
    @classmethod
    def from_points(cls, pts: ArrayLike, xaxis: ArrayLike = np.array([1,0,0]), yaxis: ArrayLike = np.array([0,1,0])) -> Self:
        """ 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. """
        m = cls()
        pts = np.asarray(pts,dtype=np.float64, order='C')
        if pts.size % 3 != 0:
            raise ValueError("from_points: pts must be a flat array with length a multiple of 3")
        xaxis = np.asarray(xaxis,dtype=np.float64, order='C')
        if xaxis.size != 3:
            raise ValueError("from_points: xaxis must be a 3D vector")
        yaxis = np.asarray(yaxis,dtype=np.float64, order='C')
        if yaxis.size != 3:
            raise ValueError("from_points: yaxis must be a 3D vector")
        m.obj = lib_py_gel.Manifold_from_points(_n_vec3(pts), pts, xaxis, yaxis)
        return m
    def __del__(self):
        lib_py_gel.Manifold_delete(self.obj)
    def merge_with(self, other: Self):
        """ 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. """
        lib_py_gel.Manifold_merge(self.obj, other.obj)
    def add_face(self, pts: ArrayLike) -> int:
        """ 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.
        """
        pts = np.asarray(pts,dtype=np.float64, order='C')
        if pts.size % 3 != 0:
            raise ValueError("add_face: pts must be a flat array with length a multiple of 3")
        return lib_py_gel.Manifold_add_face(self.obj, _n_vec3(pts), pts)
    def positions(self) -> ndarray:
        """ Retrieve an array containing the vertex positions of the Manifold.
        It is not a copy: any changes are made to the actual vertex positions. """
        pos = ct.POINTER(ct.c_double)()
        n = lib_py_gel.Manifold_positions(self.obj, ct.byref(pos))
        return np.ctypeslib.as_array(pos,(n,3))
    def no_allocated_vertices(self) -> int:
        """ 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."""
        return lib_py_gel.Manifold_no_allocated_vertices(self.obj)
    def no_allocated_faces(self) -> int:
        """ 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."""
        return lib_py_gel.Manifold_no_allocated_faces(self.obj)
    def no_allocated_halfedges(self) -> int:
        """ 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."""
        return lib_py_gel.Manifold_no_allocated_halfedges(self.obj)
    def vertices(self) -> IntVector:
        """ Returns an iterable containing all vertex indices"""
        verts = IntVector()
        lib_py_gel.Manifold_vertices(self.obj, verts.obj)
        return verts
    def faces(self) -> IntVector:
        """ Returns an iterable containing all face indices"""
        faces = IntVector()
        lib_py_gel.Manifold_faces(self.obj, faces.obj)
        return faces
    def halfedges(self) -> IntVector:
        """ Returns an iterable containing all halfedge indices"""
        hedges = IntVector()
        lib_py_gel.Manifold_halfedges(self.obj, hedges.obj)
        return hedges
    def circulate_vertex(self, vid: int, mode: str ='v') -> IntVector:
        """ 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. """
        nbrs = IntVector()
        lib_py_gel.Manifold_circulate_vertex(self.obj, vid, ct.c_char(mode.encode('ascii')), nbrs.obj)
        return nbrs
    def circulate_face(self, fid: int, mode: str ='v') -> IntVector:
        """ 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. """
        nbrs = IntVector()
        lib_py_gel.Manifold_circulate_face(self.obj, fid, ct.c_char(mode.encode('ascii')), nbrs.obj)
        return nbrs
    def next_halfedge(self, hid: int) -> int:
        """ Returns next halfedge to hid. """
        return lib_py_gel.Walker_next_halfedge(self.obj, hid)
    def prev_halfedge(self, hid: int) -> int:
        """ Returns previous halfedge to hid. """
        return lib_py_gel.Walker_prev_halfedge(self.obj, hid)
    def opposite_halfedge(self, hid: int) -> int:
        """ Returns opposite halfedge to hid. """
        return lib_py_gel.Walker_opposite_halfedge(self.obj, hid)
    def incident_face(self, hid: int) -> int:
        """ Returns face corresponding to hid. """
        return lib_py_gel.Walker_incident_face(self.obj, hid)
    def incident_vertex(self, hid: int) -> int:
        """ Returns vertex corresponding to (or pointed to by) hid. """
        return lib_py_gel.Walker_incident_vertex(self.obj, hid)
    def remove_vertex(self, vid: int) -> bool:
        """ Remove vertex vid from the Manifold. This function merges all faces
        around the vertex into one and then removes this resulting face. """
        return lib_py_gel.Manifold_remove_vertex(self.obj, vid)
    def remove_face(self, fid: int) -> bool:
        """ 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. """
        return lib_py_gel.Manifold_remove_face(self.obj, fid)
    def remove_edge(self, hid: int) -> bool:
        """ 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. """
        return lib_py_gel.Manifold_remove_edge(self.obj, hid)
    def vertex_in_use(self, vid: int) -> bool:
        """ 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 invalid or it could correspond to a vertex which is not active. The function returns 
        false in both cases.  It is important to call this function before using a vertex id (vid)
        which might have been invalidated by a previous operation on the mesh."""
        return lib_py_gel.Manifold_vertex_in_use(self.obj, vid)
    def face_in_use(self, fid: int) -> bool:
        """ 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 invalid or it could correspond to a face which is not active. The function returns 
        false in both cases. It is important to call this function before using a face id (fid)
        which might have been invalidated by a previous operation on the mesh.
        """
        return lib_py_gel.Manifold_face_in_use(self.obj, fid)
    def halfedge_in_use(self, hid: int) -> bool:
        """ 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 invalid or it could correspond to a halfedge which is not active. The function returns 
        false in both cases.  It is important to call this function before using a halfedge id (hid)
        which might have been invalidated by a previous operation on the mesh."""
        return lib_py_gel.Manifold_halfedge_in_use(self.obj, hid)
    def flip_edge(self, hid: int) -> bool:
        """ 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."""
        return lib_py_gel.Manifold_flip_edge(self.obj,hid)
    def collapse_edge(self, hid: int, avg_vertices: bool = False) -> bool:
        """ Collapse an edge hid.
        The vertex incident_vertex(opposite_halfedge) is the one being removed 
        while incident_vertex(hid) survives. avg_vertices indicates whether the
        positions of the two vertices should be averaged when collapsed.
        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."""
        return lib_py_gel.Manifold_collapse_edge(self.obj, hid, avg_vertices)
    def split_face_by_edge(self, fid: int, v0: int, v1: int) -> int:
        """   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. """
        return lib_py_gel.Manifold_split_face_by_edge(self.obj, fid, v0, v1)
    def split_face_by_vertex(self,fid: int) -> int:
        """   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. """
        return lib_py_gel.Manifold_split_face_by_vertex(self.obj,fid)
    def split_edge(self,hid: int) -> int:
        """   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. """
        return lib_py_gel.Manifold_split_edge(self.obj,hid)
    def stitch_boundary_edges(self,h0: int, h1: int) -> bool:
        """   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. """
        return lib_py_gel.Manifold_stitch_boundary_edges(self.obj, h0, h1)
    def merge_faces(self,hid: int) -> bool:
        """   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. """
        if self.is_halfedge_at_boundary(hid):
            return False
        fid = self.incident_face(hid)
        return lib_py_gel.Manifold_merge_faces(self.obj, fid, hid)
    def close_hole(self,hid: int) -> int:
        """ 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. """
        return lib_py_gel.Manifold_close_hole(self.obj, hid)
    def cleanup(self):
        """ 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."""
        lib_py_gel.Manifold_cleanup(self.obj)
    def is_halfedge_at_boundary(self, hid: int) -> bool:
        """ Returns True if hid is a boundary halfedge, i.e. face on either
        side is invalid. """
        return lib_py_gel.is_halfedge_at_boundary(self.obj, hid)
    def is_vertex_at_boundary(self, vid: int) -> bool:
        """ Returns True if vid lies on a boundary. """
        return lib_py_gel.is_vertex_at_boundary(self.obj, vid)
    def edge_length(self, hid: int) -> float:
        """ Returns length of edge given by halfedge hid which is passed as argument. """
        return lib_py_gel.length(self.obj, hid)
    def valency(self,vid: int) -> int:
        """ Returns valency of vid, i.e. number of incident edges."""
        return lib_py_gel.valency(self.obj,vid)
    def face_normal(self, fid: int) -> ndarray:
        """ Compute the normal of a face fid. The normal is the average of the normals
        of the triangles formed from the centroid of face fix and each edge of the face."""
        n = ndarray(3, dtype=np.float64)
        lib_py_gel.face_normal(self.obj, fid, n)
        return n
    def vertex_normal(self, vid: int) -> ndarray:
        """ Returns the vertex normal of vid. The vertex normal is computed as the
        angle weighted average of the normals of the incident faces."""
        n = ndarray(3,dtype=np.float64)
        lib_py_gel.vertex_normal(self.obj, vid, n)
        return n
    def mixed_area(self, vid: int) -> float:
        """ Returns the mixed area of vertex vid. The mixed area is an approximation
        of the Voronoi area of the vertex, i.e. the area of the mesh that is closer
        to vid than to any other vertex. For non-obtuse triangles, we can compute the
        part of the Voronoi area inside the triangle exacxtly, but for obtuse triangles
        the Voronoi area extends outside the triangle, and we approximate it. """
        return lib_py_gel.mixed_area(self.obj, vid)
    def gaussian_curvature(self, vid: int) -> float:
        """ Returns the Gaussian curvature of vertex vid. The curvature is computed
        as the ratio of the angle defect and the mixed area of vid.
        The angle defect is 2*pi minus the sum of angles at the vertex. """
        return lib_py_gel.gaussian_curvature(self.obj, vid)
    def mean_curvature(self, vid: int) -> float:
        """ Returns the mean curvature of vertex vid. The curvature is computed
        as the ratio of the length of the mean curvaure normal to the mixed area
        of vid. The mean curvature normal is obtained with the cotan formula, and 
        the sign is positive if the mean curvature normal points in the same direction
        as the vertex normal and negative otherwise. """
        return lib_py_gel.mean_curvature(self.obj, vid)
    def principal_curvatures(self, vid: int) -> tuple[float, float, ndarray, ndarray]:
        """ Returns the principal curvatures of vertex vid. The principal curvatures
        are computed by fitting a quadratic polynomial surface to the vertex and its
        one-ring neighbours. From the coefficients, we obtain the shape operator and 
        the principal curvatures are the eigenvalues of the shape operator. The directions
        are the eigenvectors. The function returns a tuple consiting of four values: 
        min and max principal curvature followed by the corresponding principal directions 
        as 3D vectors"""
        pc_data = ndarray(8, dtype=np.float64)
        lib_py_gel.principal_curvatures(self.obj, vid, pc_data)
        return (
            pc_data[0],  # min curvature
            pc_data[1],  # max curvature
            pc_data[2:5],  # min direction
            pc_data[5:8]   # max direction
        )
    def connected(self, v0: int, v1: int) -> bool:
        """ Returns true if the two argument vertices, v0 and v1, are in each other's one-rings."""
        return lib_py_gel.connected(self.obj,v0,v1)
    def no_edges(self, fid: int) -> int:
        """ Compute the number of edges of a face fid """
        return lib_py_gel.no_edges(self.obj, fid)
    def area(self, fid: int) -> float:
        """ Returns the area of a face fid. """
        return lib_py_gel.area(self.obj, fid)
    def perimeter(self, fid: int) -> float:
        """ Returns the perimeter of a face fid. """
        return lib_py_gel.perimeter(self.obj, fid)
    def centre(self, fid: int) -> ndarray:
        """ Returns the centre of a face. """
        c = ndarray(3, dtype=np.float64)
        lib_py_gel.centre(self.obj, fid, c)
        return c

__init__

__init__(orig: Self | c_void_p | None = None)

Construct a Manifold object. If orig is None, a new empty Manifold is created. If orig is a Manifold, a copy of it is created. If orig is a c_void_p, it is assumed to be a pointer to a Manifold object created in C++. In this case, the object is not copied, but the pointer is used directly. If orig is anything else, a TypeError is raised.

Source code in pygel3d/hmesh.py
def __init__(self, orig: Self | ct.c_void_p | None = None):
    """ Construct a Manifold object. If orig is None, a new empty Manifold is created. If
    orig is a Manifold, a copy of it is created. If orig is a c_void_p, it is assumed to be
    a pointer to a Manifold object created in C++. In this case, the object is not copied,
    but the pointer is used directly. If orig is anything else, a TypeError is raised.
    """
    if orig == None:
        self.obj = lib_py_gel.Manifold_new()
    elif isinstance(orig, Manifold):
        self.obj = lib_py_gel.Manifold_copy(orig.obj)
    elif isinstance(orig, ct.c_void_p):
        self.obj = orig
    else:
        raise TypeError(f"Manifold constructor takes either a Manifold or a c_void_p as argument, not {type(orig)}")

from_triangles classmethod

from_triangles(vertices: ArrayLike, faces: ArrayLike) -> Self

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

Source code in pygel3d/hmesh.py
@classmethod
def from_triangles(cls, vertices: ArrayLike, faces: ArrayLike) -> Self:
    """ Given a list of vertices and triangles (faces), this function produces
    a Manifold mesh."""
    m = cls()
    vertices = np.asarray(vertices,dtype=np.float64, order='C')
    faces = np.asarray(faces,dtype=ct.c_int, order='C')
    m.obj = lib_py_gel.Manifold_from_triangles(_n_vec3(vertices), _n_vec3(faces), vertices, faces)
    return m

from_points classmethod

from_points(pts: ArrayLike, xaxis: ArrayLike = np.array([1, 0, 0]), yaxis: ArrayLike = np.array([0, 1, 0])) -> Self

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.

Source code in pygel3d/hmesh.py
@classmethod
def from_points(cls, pts: ArrayLike, xaxis: ArrayLike = np.array([1,0,0]), yaxis: ArrayLike = np.array([0,1,0])) -> Self:
    """ 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. """
    m = cls()
    pts = np.asarray(pts,dtype=np.float64, order='C')
    if pts.size % 3 != 0:
        raise ValueError("from_points: pts must be a flat array with length a multiple of 3")
    xaxis = np.asarray(xaxis,dtype=np.float64, order='C')
    if xaxis.size != 3:
        raise ValueError("from_points: xaxis must be a 3D vector")
    yaxis = np.asarray(yaxis,dtype=np.float64, order='C')
    if yaxis.size != 3:
        raise ValueError("from_points: yaxis must be a 3D vector")
    m.obj = lib_py_gel.Manifold_from_points(_n_vec3(pts), pts, xaxis, yaxis)
    return m

merge_with

merge_with(other: Self)

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.

Source code in pygel3d/hmesh.py
def merge_with(self, other: Self):
    """ 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. """
    lib_py_gel.Manifold_merge(self.obj, other.obj)

add_face

add_face(pts: ArrayLike) -> int

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.

Source code in pygel3d/hmesh.py
def add_face(self, pts: ArrayLike) -> int:
    """ 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.
    """
    pts = np.asarray(pts,dtype=np.float64, order='C')
    if pts.size % 3 != 0:
        raise ValueError("add_face: pts must be a flat array with length a multiple of 3")
    return lib_py_gel.Manifold_add_face(self.obj, _n_vec3(pts), pts)

positions

positions() -> ndarray

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

Source code in pygel3d/hmesh.py
def positions(self) -> ndarray:
    """ Retrieve an array containing the vertex positions of the Manifold.
    It is not a copy: any changes are made to the actual vertex positions. """
    pos = ct.POINTER(ct.c_double)()
    n = lib_py_gel.Manifold_positions(self.obj, ct.byref(pos))
    return np.ctypeslib.as_array(pos,(n,3))

no_allocated_vertices

no_allocated_vertices() -> int

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.

Source code in pygel3d/hmesh.py
def no_allocated_vertices(self) -> int:
    """ 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."""
    return lib_py_gel.Manifold_no_allocated_vertices(self.obj)

no_allocated_faces

no_allocated_faces() -> int

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.

Source code in pygel3d/hmesh.py
def no_allocated_faces(self) -> int:
    """ 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."""
    return lib_py_gel.Manifold_no_allocated_faces(self.obj)

no_allocated_halfedges

no_allocated_halfedges() -> int

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.

Source code in pygel3d/hmesh.py
def no_allocated_halfedges(self) -> int:
    """ 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."""
    return lib_py_gel.Manifold_no_allocated_halfedges(self.obj)

vertices

vertices() -> IntVector

Returns an iterable containing all vertex indices

Source code in pygel3d/hmesh.py
def vertices(self) -> IntVector:
    """ Returns an iterable containing all vertex indices"""
    verts = IntVector()
    lib_py_gel.Manifold_vertices(self.obj, verts.obj)
    return verts

faces

faces() -> IntVector

Returns an iterable containing all face indices

Source code in pygel3d/hmesh.py
def faces(self) -> IntVector:
    """ Returns an iterable containing all face indices"""
    faces = IntVector()
    lib_py_gel.Manifold_faces(self.obj, faces.obj)
    return faces

halfedges

halfedges() -> IntVector

Returns an iterable containing all halfedge indices

Source code in pygel3d/hmesh.py
def halfedges(self) -> IntVector:
    """ Returns an iterable containing all halfedge indices"""
    hedges = IntVector()
    lib_py_gel.Manifold_halfedges(self.obj, hedges.obj)
    return hedges

circulate_vertex

circulate_vertex(vid: int, mode: str = 'v') -> IntVector

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.

Source code in pygel3d/hmesh.py
def circulate_vertex(self, vid: int, mode: str ='v') -> IntVector:
    """ 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. """
    nbrs = IntVector()
    lib_py_gel.Manifold_circulate_vertex(self.obj, vid, ct.c_char(mode.encode('ascii')), nbrs.obj)
    return nbrs

circulate_face

circulate_face(fid: int, mode: str = 'v') -> IntVector

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.

Source code in pygel3d/hmesh.py
def circulate_face(self, fid: int, mode: str ='v') -> IntVector:
    """ 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. """
    nbrs = IntVector()
    lib_py_gel.Manifold_circulate_face(self.obj, fid, ct.c_char(mode.encode('ascii')), nbrs.obj)
    return nbrs

next_halfedge

next_halfedge(hid: int) -> int

Returns next halfedge to hid.

Source code in pygel3d/hmesh.py
def next_halfedge(self, hid: int) -> int:
    """ Returns next halfedge to hid. """
    return lib_py_gel.Walker_next_halfedge(self.obj, hid)

prev_halfedge

prev_halfedge(hid: int) -> int

Returns previous halfedge to hid.

Source code in pygel3d/hmesh.py
def prev_halfedge(self, hid: int) -> int:
    """ Returns previous halfedge to hid. """
    return lib_py_gel.Walker_prev_halfedge(self.obj, hid)

opposite_halfedge

opposite_halfedge(hid: int) -> int

Returns opposite halfedge to hid.

Source code in pygel3d/hmesh.py
def opposite_halfedge(self, hid: int) -> int:
    """ Returns opposite halfedge to hid. """
    return lib_py_gel.Walker_opposite_halfedge(self.obj, hid)

incident_face

incident_face(hid: int) -> int

Returns face corresponding to hid.

Source code in pygel3d/hmesh.py
def incident_face(self, hid: int) -> int:
    """ Returns face corresponding to hid. """
    return lib_py_gel.Walker_incident_face(self.obj, hid)

incident_vertex

incident_vertex(hid: int) -> int

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

Source code in pygel3d/hmesh.py
def incident_vertex(self, hid: int) -> int:
    """ Returns vertex corresponding to (or pointed to by) hid. """
    return lib_py_gel.Walker_incident_vertex(self.obj, hid)

remove_vertex

remove_vertex(vid: int) -> bool

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

Source code in pygel3d/hmesh.py
def remove_vertex(self, vid: int) -> bool:
    """ Remove vertex vid from the Manifold. This function merges all faces
    around the vertex into one and then removes this resulting face. """
    return lib_py_gel.Manifold_remove_vertex(self.obj, vid)

remove_face

remove_face(fid: int) -> bool

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.

Source code in pygel3d/hmesh.py
def remove_face(self, fid: int) -> bool:
    """ 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. """
    return lib_py_gel.Manifold_remove_face(self.obj, fid)

remove_edge

remove_edge(hid: int) -> bool

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.

Source code in pygel3d/hmesh.py
def remove_edge(self, hid: int) -> bool:
    """ 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. """
    return lib_py_gel.Manifold_remove_edge(self.obj, hid)

vertex_in_use

vertex_in_use(vid: int) -> bool

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 invalid or it could correspond to a vertex which is not active. The function returns false in both cases. It is important to call this function before using a vertex id (vid) which might have been invalidated by a previous operation on the mesh.

Source code in pygel3d/hmesh.py
def vertex_in_use(self, vid: int) -> bool:
    """ 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 invalid or it could correspond to a vertex which is not active. The function returns 
    false in both cases.  It is important to call this function before using a vertex id (vid)
    which might have been invalidated by a previous operation on the mesh."""
    return lib_py_gel.Manifold_vertex_in_use(self.obj, vid)

face_in_use

face_in_use(fid: int) -> bool

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 invalid or it could correspond to a face which is not active. The function returns false in both cases. It is important to call this function before using a face id (fid) which might have been invalidated by a previous operation on the mesh.

Source code in pygel3d/hmesh.py
def face_in_use(self, fid: int) -> bool:
    """ 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 invalid or it could correspond to a face which is not active. The function returns 
    false in both cases. It is important to call this function before using a face id (fid)
    which might have been invalidated by a previous operation on the mesh.
    """
    return lib_py_gel.Manifold_face_in_use(self.obj, fid)

halfedge_in_use

halfedge_in_use(hid: int) -> bool

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 invalid or it could correspond to a halfedge which is not active. The function returns false in both cases. It is important to call this function before using a halfedge id (hid) which might have been invalidated by a previous operation on the mesh.

Source code in pygel3d/hmesh.py
def halfedge_in_use(self, hid: int) -> bool:
    """ 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 invalid or it could correspond to a halfedge which is not active. The function returns 
    false in both cases.  It is important to call this function before using a halfedge id (hid)
    which might have been invalidated by a previous operation on the mesh."""
    return lib_py_gel.Manifold_halfedge_in_use(self.obj, hid)

flip_edge

flip_edge(hid: int) -> bool

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.

Source code in pygel3d/hmesh.py
def flip_edge(self, hid: int) -> bool:
    """ 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."""
    return lib_py_gel.Manifold_flip_edge(self.obj,hid)

collapse_edge

collapse_edge(hid: int, avg_vertices: bool = False) -> bool

Collapse an edge hid. The vertex incident_vertex(opposite_halfedge) is the one being removed while incident_vertex(hid) survives. avg_vertices indicates whether the positions of the two vertices should be averaged when collapsed. 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.
Source code in pygel3d/hmesh.py
def collapse_edge(self, hid: int, avg_vertices: bool = False) -> bool:
    """ Collapse an edge hid.
    The vertex incident_vertex(opposite_halfedge) is the one being removed 
    while incident_vertex(hid) survives. avg_vertices indicates whether the
    positions of the two vertices should be averaged when collapsed.
    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."""
    return lib_py_gel.Manifold_collapse_edge(self.obj, hid, avg_vertices)

split_face_by_edge

split_face_by_edge(fid: int, v0: int, v1: int) -> int

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.

Source code in pygel3d/hmesh.py
def split_face_by_edge(self, fid: int, v0: int, v1: int) -> int:
    """   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. """
    return lib_py_gel.Manifold_split_face_by_edge(self.obj, fid, v0, v1)

split_face_by_vertex

split_face_by_vertex(fid: int) -> int

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.

Source code in pygel3d/hmesh.py
def split_face_by_vertex(self,fid: int) -> int:
    """   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. """
    return lib_py_gel.Manifold_split_face_by_vertex(self.obj,fid)

split_edge

split_edge(hid: int) -> int

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.

Source code in pygel3d/hmesh.py
def split_edge(self,hid: int) -> int:
    """   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. """
    return lib_py_gel.Manifold_split_edge(self.obj,hid)

stitch_boundary_edges

stitch_boundary_edges(h0: int, h1: int) -> bool

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.

Source code in pygel3d/hmesh.py
def stitch_boundary_edges(self,h0: int, h1: int) -> bool:
    """   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. """
    return lib_py_gel.Manifold_stitch_boundary_edges(self.obj, h0, h1)

merge_faces

merge_faces(hid: int) -> bool

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.

Source code in pygel3d/hmesh.py
def merge_faces(self,hid: int) -> bool:
    """   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. """
    if self.is_halfedge_at_boundary(hid):
        return False
    fid = self.incident_face(hid)
    return lib_py_gel.Manifold_merge_faces(self.obj, fid, hid)

close_hole

close_hole(hid: int) -> int

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.

Source code in pygel3d/hmesh.py
def close_hole(self,hid: int) -> int:
    """ 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. """
    return lib_py_gel.Manifold_close_hole(self.obj, hid)

cleanup

cleanup()

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.

Source code in pygel3d/hmesh.py
def cleanup(self):
    """ 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."""
    lib_py_gel.Manifold_cleanup(self.obj)

is_halfedge_at_boundary

is_halfedge_at_boundary(hid: int) -> bool

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

Source code in pygel3d/hmesh.py
def is_halfedge_at_boundary(self, hid: int) -> bool:
    """ Returns True if hid is a boundary halfedge, i.e. face on either
    side is invalid. """
    return lib_py_gel.is_halfedge_at_boundary(self.obj, hid)

is_vertex_at_boundary

is_vertex_at_boundary(vid: int) -> bool

Returns True if vid lies on a boundary.

Source code in pygel3d/hmesh.py
def is_vertex_at_boundary(self, vid: int) -> bool:
    """ Returns True if vid lies on a boundary. """
    return lib_py_gel.is_vertex_at_boundary(self.obj, vid)

edge_length

edge_length(hid: int) -> float

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

Source code in pygel3d/hmesh.py
def edge_length(self, hid: int) -> float:
    """ Returns length of edge given by halfedge hid which is passed as argument. """
    return lib_py_gel.length(self.obj, hid)

valency

valency(vid: int) -> int

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

Source code in pygel3d/hmesh.py
def valency(self,vid: int) -> int:
    """ Returns valency of vid, i.e. number of incident edges."""
    return lib_py_gel.valency(self.obj,vid)

face_normal

face_normal(fid: int) -> ndarray

Compute the normal of a face fid. The normal is the average of the normals of the triangles formed from the centroid of face fix and each edge of the face.

Source code in pygel3d/hmesh.py
def face_normal(self, fid: int) -> ndarray:
    """ Compute the normal of a face fid. The normal is the average of the normals
    of the triangles formed from the centroid of face fix and each edge of the face."""
    n = ndarray(3, dtype=np.float64)
    lib_py_gel.face_normal(self.obj, fid, n)
    return n

vertex_normal

vertex_normal(vid: int) -> ndarray

Returns the vertex normal of vid. The vertex normal is computed as the angle weighted average of the normals of the incident faces.

Source code in pygel3d/hmesh.py
def vertex_normal(self, vid: int) -> ndarray:
    """ Returns the vertex normal of vid. The vertex normal is computed as the
    angle weighted average of the normals of the incident faces."""
    n = ndarray(3,dtype=np.float64)
    lib_py_gel.vertex_normal(self.obj, vid, n)
    return n

mixed_area

mixed_area(vid: int) -> float

Returns the mixed area of vertex vid. The mixed area is an approximation of the Voronoi area of the vertex, i.e. the area of the mesh that is closer to vid than to any other vertex. For non-obtuse triangles, we can compute the part of the Voronoi area inside the triangle exacxtly, but for obtuse triangles the Voronoi area extends outside the triangle, and we approximate it.

Source code in pygel3d/hmesh.py
def mixed_area(self, vid: int) -> float:
    """ Returns the mixed area of vertex vid. The mixed area is an approximation
    of the Voronoi area of the vertex, i.e. the area of the mesh that is closer
    to vid than to any other vertex. For non-obtuse triangles, we can compute the
    part of the Voronoi area inside the triangle exacxtly, but for obtuse triangles
    the Voronoi area extends outside the triangle, and we approximate it. """
    return lib_py_gel.mixed_area(self.obj, vid)

gaussian_curvature

gaussian_curvature(vid: int) -> float

Returns the Gaussian curvature of vertex vid. The curvature is computed as the ratio of the angle defect and the mixed area of vid. The angle defect is 2*pi minus the sum of angles at the vertex.

Source code in pygel3d/hmesh.py
def gaussian_curvature(self, vid: int) -> float:
    """ Returns the Gaussian curvature of vertex vid. The curvature is computed
    as the ratio of the angle defect and the mixed area of vid.
    The angle defect is 2*pi minus the sum of angles at the vertex. """
    return lib_py_gel.gaussian_curvature(self.obj, vid)

mean_curvature

mean_curvature(vid: int) -> float

Returns the mean curvature of vertex vid. The curvature is computed as the ratio of the length of the mean curvaure normal to the mixed area of vid. The mean curvature normal is obtained with the cotan formula, and the sign is positive if the mean curvature normal points in the same direction as the vertex normal and negative otherwise.

Source code in pygel3d/hmesh.py
def mean_curvature(self, vid: int) -> float:
    """ Returns the mean curvature of vertex vid. The curvature is computed
    as the ratio of the length of the mean curvaure normal to the mixed area
    of vid. The mean curvature normal is obtained with the cotan formula, and 
    the sign is positive if the mean curvature normal points in the same direction
    as the vertex normal and negative otherwise. """
    return lib_py_gel.mean_curvature(self.obj, vid)

principal_curvatures

principal_curvatures(vid: int) -> tuple[float, float, ndarray, ndarray]

Returns the principal curvatures of vertex vid. The principal curvatures are computed by fitting a quadratic polynomial surface to the vertex and its one-ring neighbours. From the coefficients, we obtain the shape operator and the principal curvatures are the eigenvalues of the shape operator. The directions are the eigenvectors. The function returns a tuple consiting of four values: min and max principal curvature followed by the corresponding principal directions as 3D vectors

Source code in pygel3d/hmesh.py
def principal_curvatures(self, vid: int) -> tuple[float, float, ndarray, ndarray]:
    """ Returns the principal curvatures of vertex vid. The principal curvatures
    are computed by fitting a quadratic polynomial surface to the vertex and its
    one-ring neighbours. From the coefficients, we obtain the shape operator and 
    the principal curvatures are the eigenvalues of the shape operator. The directions
    are the eigenvectors. The function returns a tuple consiting of four values: 
    min and max principal curvature followed by the corresponding principal directions 
    as 3D vectors"""
    pc_data = ndarray(8, dtype=np.float64)
    lib_py_gel.principal_curvatures(self.obj, vid, pc_data)
    return (
        pc_data[0],  # min curvature
        pc_data[1],  # max curvature
        pc_data[2:5],  # min direction
        pc_data[5:8]   # max direction
    )

connected

connected(v0: int, v1: int) -> bool

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

Source code in pygel3d/hmesh.py
def connected(self, v0: int, v1: int) -> bool:
    """ Returns true if the two argument vertices, v0 and v1, are in each other's one-rings."""
    return lib_py_gel.connected(self.obj,v0,v1)

no_edges

no_edges(fid: int) -> int

Compute the number of edges of a face fid

Source code in pygel3d/hmesh.py
def no_edges(self, fid: int) -> int:
    """ Compute the number of edges of a face fid """
    return lib_py_gel.no_edges(self.obj, fid)

area

area(fid: int) -> float

Returns the area of a face fid.

Source code in pygel3d/hmesh.py
def area(self, fid: int) -> float:
    """ Returns the area of a face fid. """
    return lib_py_gel.area(self.obj, fid)

perimeter

perimeter(fid: int) -> float

Returns the perimeter of a face fid.

Source code in pygel3d/hmesh.py
def perimeter(self, fid: int) -> float:
    """ Returns the perimeter of a face fid. """
    return lib_py_gel.perimeter(self.obj, fid)

centre

centre(fid: int) -> ndarray

Returns the centre of a face.

Source code in pygel3d/hmesh.py
def centre(self, fid: int) -> ndarray:
    """ Returns the centre of a face. """
    c = ndarray(3, dtype=np.float64)
    lib_py_gel.centre(self.obj, fid, c)
    return c

MeshDistance

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.

Source code in pygel3d/hmesh.py
class MeshDistance:
    """ 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 __init__(self,m: Manifold):
        self.obj = lib_py_gel.MeshDistance_new(m.obj)
    def __del__(self):
        lib_py_gel.MeshDistance_delete(self.obj)
    def signed_distance(self, pts: ArrayLike, upper: float = 1e30) -> np.ndarray:
        """ 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. """
        p = np.asarray(pts, dtype=ct.c_float, order='C')
        ndim = len(p.shape)
        if ndim==1:
            n = p.shape[0]//3
        elif ndim==2:
            n = p.shape[0]
        else:
            raise ValueError("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")

        d = np.ndarray(n, dtype=ct.c_float)
        lib_py_gel.MeshDistance_signed_distance(self.obj, n, p, d, upper)
        return d[0] if n==1 else d
    def ray_inside_test(self, pts: ArrayLike, no_rays: int = 3) -> np.ndarray:
        """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). """
        p = np.asarray(pts, dtype=ct.c_float, order='C')
        ndim = len(p.shape)
        if ndim==1:
            n = p.shape[0]//3
        elif ndim==2:
            n = p.shape[0]
        else:
            raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
        s = np.ndarray(n, dtype=ct.c_int)
        lib_py_gel.MeshDistance_ray_inside_test(self.obj,n,p,s,no_rays)
        return s[0] if n==1 else s
    def intersect(self, p0: ArrayLike, dir: ArrayLike, _t: float = 0) -> tuple[float, np.ndarray, np.ndarray] | 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. """
        p0 = np.asarray(p0,dtype=ct.c_float)
        dir = np.asarray(dir,dtype=ct.c_float)
        t = ct.c_float(_t)
        r = lib_py_gel.MeshDistance_ray_intersect(self.obj, p0, dir, ct.byref(t))
        if r:
            return t.value, p0, dir
        return None

signed_distance

signed_distance(pts: ArrayLike, upper: float = 1e+30) -> np.ndarray

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.

Source code in pygel3d/hmesh.py
def signed_distance(self, pts: ArrayLike, upper: float = 1e30) -> np.ndarray:
    """ 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. """
    p = np.asarray(pts, dtype=ct.c_float, order='C')
    ndim = len(p.shape)
    if ndim==1:
        n = p.shape[0]//3
    elif ndim==2:
        n = p.shape[0]
    else:
        raise ValueError("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")

    d = np.ndarray(n, dtype=ct.c_float)
    lib_py_gel.MeshDistance_signed_distance(self.obj, n, p, d, upper)
    return d[0] if n==1 else d

ray_inside_test

ray_inside_test(pts: ArrayLike, no_rays: int = 3) -> np.ndarray

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).

Source code in pygel3d/hmesh.py
def ray_inside_test(self, pts: ArrayLike, no_rays: int = 3) -> np.ndarray:
    """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). """
    p = np.asarray(pts, dtype=ct.c_float, order='C')
    ndim = len(p.shape)
    if ndim==1:
        n = p.shape[0]//3
    elif ndim==2:
        n = p.shape[0]
    else:
        raise Exception("you must pass signed_distance pts as a 1D array or a 2D array of dim nx3")
    s = np.ndarray(n, dtype=ct.c_int)
    lib_py_gel.MeshDistance_ray_inside_test(self.obj,n,p,s,no_rays)
    return s[0] if n==1 else s

intersect

intersect(p0: ArrayLike, dir: ArrayLike, _t: float = 0) -> tuple[float, np.ndarray, np.ndarray] | 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.

Source code in pygel3d/hmesh.py
def intersect(self, p0: ArrayLike, dir: ArrayLike, _t: float = 0) -> tuple[float, np.ndarray, np.ndarray] | 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. """
    p0 = np.asarray(p0,dtype=ct.c_float)
    dir = np.asarray(dir,dtype=ct.c_float)
    t = ct.c_float(_t)
    r = lib_py_gel.MeshDistance_ray_intersect(self.obj, p0, dir, ct.byref(t))
    if r:
        return t.value, p0, dir
    return None

valid

valid(m: Manifold) -> bool

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.

Source code in pygel3d/hmesh.py
def valid(m: Manifold) -> bool:
    """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. """
    return lib_py_gel.valid(m.obj)

closed

closed(m: Manifold) -> bool

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

Source code in pygel3d/hmesh.py
def closed(m: Manifold) -> bool:
    """ Returns true if m is closed, i.e. has no boundary."""
    return lib_py_gel.closed(m.obj)

area

area(m: Manifold) -> float

This function computes the sum of all the faces' areas

Source code in pygel3d/hmesh.py
def area(m: Manifold) -> float:
    """ This function computes the sum of all the faces' areas """
    return lib_py_gel.total_area(m.obj)

volume

volume(m: Manifold) -> float

Computes the volume of a mesh. Presupposes that the mesh is closed.

Source code in pygel3d/hmesh.py
def volume(m: Manifold) -> float:
    """ Computes the volume of a mesh. Presupposes that the mesh is closed. """
    return lib_py_gel.volume(m.obj)

bbox

bbox(m: Manifold) -> tuple[ndarray, ndarray]

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

Source code in pygel3d/hmesh.py
def bbox(m: Manifold) -> tuple[ndarray, ndarray]:
    """ Returns the min and max corners of the bounding box of Manifold m. """
    pmin = ndarray(3,dtype=np.float64)
    pmax = ndarray(3,dtype=np.float64)
    lib_py_gel.bbox(m.obj, pmin, pmax)
    return pmin, pmax

bsphere

bsphere(m: Manifold) -> tuple[ndarray, float]

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

Source code in pygel3d/hmesh.py
def bsphere(m: Manifold) -> tuple[ndarray, float]:
    """ Calculate the bounding sphere of the manifold m.
    Returns centre,radius """
    c = ndarray(3,dtype=np.float64)
    r = ct.c_double()
    lib_py_gel.bsphere(m.obj, c, ct.byref(r))
    return (c,r.value)

stitch

stitch(m: Manifold, rad: float = 1e-30) -> int

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.

Source code in pygel3d/hmesh.py
def stitch(m: Manifold, rad: float = 1e-30) -> int:
    """ 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."""
    return lib_py_gel.stitch_mesh(m.obj,rad)

obj_save

obj_save(fn: str, m: Manifold)

Save Manifold m to Wavefront obj file.

Source code in pygel3d/hmesh.py
def obj_save(fn: str, m: Manifold):
    """ Save Manifold m to Wavefront obj file. """
    s = ct.c_char_p(fn.encode('utf-8'))
    lib_py_gel.obj_save(s, m.obj)

off_save

off_save(fn: str, m: Manifold)

Save Manifold m to OFF file.

Source code in pygel3d/hmesh.py
def off_save(fn: str, m: Manifold):
    """ Save Manifold m to OFF file. """
    s = ct.c_char_p(fn.encode('utf-8'))
    lib_py_gel.off_save(s, m.obj)

x3d_save

x3d_save(fn: str, m: Manifold)

Save Manifold m to X3D file.

Source code in pygel3d/hmesh.py
def x3d_save(fn: str, m: Manifold):
    """ Save Manifold m to X3D file. """
    s = ct.c_char_p(fn.encode('utf-8'))
    lib_py_gel.x3d_save(s, m.obj)

obj_load

obj_load(fn: str) -> Manifold | None

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

Source code in pygel3d/hmesh.py
def obj_load(fn: str) -> Manifold | None:
    """ Load and return Manifold from Wavefront obj file.
    Returns None if loading failed. """
    m = Manifold()
    s = ct.c_char_p(fn.encode('utf-8'))
    if lib_py_gel.obj_load(s, m.obj):
        return m
    return None

off_load

off_load(fn: str) -> Manifold | None

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

Source code in pygel3d/hmesh.py
def off_load(fn: str) -> Manifold | None:
    """ Load and return Manifold from OFF file.
    Returns None if loading failed."""
    m = Manifold()
    s = ct.c_char_p(fn.encode('utf-8'))
    if lib_py_gel.off_load(s, m.obj):
        return m
    return None

ply_load

ply_load(fn: str) -> Manifold | None

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

Source code in pygel3d/hmesh.py
def ply_load(fn: str) -> Manifold | None:
    """ Load and return Manifold from Stanford PLY file.
    Returns None if loading failed. """
    m = Manifold()
    s = ct.c_char_p(fn.encode('utf-8'))
    if lib_py_gel.ply_load(s, m.obj):
        return m
    return None

x3d_load

x3d_load(fn: str) -> Manifold | None

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

Source code in pygel3d/hmesh.py
def x3d_load(fn: str) -> Manifold | None:
    """ Load and return Manifold from X3D file.
    Returns None if loading failed."""
    m = Manifold()
    s = ct.c_char_p(fn.encode('utf-8'))
    if lib_py_gel.x3d_load(s, m.obj):
        return m
    return None

load

load(fn: str) -> Manifold | None

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

Source code in pygel3d/hmesh.py
def load(fn: str) -> Manifold | None:
    """ Load a Manifold from an X3D/OBJ/OFF/PLY file. Return the
    loaded Manifold. Returns None if loading failed."""
    _, extension = splitext(fn)
    if extension.lower() == ".x3d":
        return x3d_load(fn)
    if extension.lower() == ".obj":
        return obj_load(fn)
    if extension.lower() == ".off":
        return off_load(fn)
    if extension.lower() == ".ply":
        return ply_load(fn)
    return None

save

save(fn: str, m: Manifold)

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

Source code in pygel3d/hmesh.py
def save(fn: str, m: Manifold):
    """ Save a Manifold, m, to an X3D/OBJ/OFF file. """
    _, extension = splitext(fn)
    ext = extension.lower()
    if ext == ".x3d":
        x3d_save(fn, m)
    elif ext == ".obj":
        obj_save(fn, m)
    elif ext == ".off":
        off_save(fn, m)
    else:
        raise ValueError(
            f"hmesh.save: unsupported format '{extension}'. "
            "Use .obj, .off, or .x3d"
        )

remove_caps

remove_caps(m: Manifold, thresh: float = 2.9)

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

Source code in pygel3d/hmesh.py
def remove_caps(m: Manifold, thresh: float = 2.9):
    """ 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 !!! """
    lib_py_gel.remove_caps(m.obj,thresh)

remove_needles

remove_needles(m: Manifold, thresh: float = 0.05, average_positions: bool = False)

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.

Source code in pygel3d/hmesh.py
def remove_needles(m: Manifold, thresh: float = 0.05, average_positions: bool = False):
    """  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."""
    abs_thresh = thresh * average_edge_length(m)
    lib_py_gel.remove_needles(m.obj,abs_thresh, average_positions)

close_holes

close_holes(m: Manifold, max_size: int = 100)

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.

Source code in pygel3d/hmesh.py
def close_holes(m: Manifold, max_size: int = 100):
    """  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. """
    lib_py_gel.close_holes(m.obj, max_size)

flip_orientation

flip_orientation(m: Manifold)

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

Source code in pygel3d/hmesh.py
def flip_orientation(m: Manifold):
    """  Flip the orientation of a mesh, m. After calling this function, normals
    will point the other way and clockwise becomes counter clockwise """
    lib_py_gel.flip_orientation(m.obj)

merge_coincident_boundary_vertices

merge_coincident_boundary_vertices(m: Manifold, rad: float = 1e-30)

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.

Source code in pygel3d/hmesh.py
def merge_coincident_boundary_vertices(m: Manifold, rad: float = 1.0e-30):
    """  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. """
    lib_py_gel.merge_coincident_boundary_vertices(m.obj, rad)

minimize_curvature

minimize_curvature(m: Manifold, anneal: bool = False)

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.

Source code in pygel3d/hmesh.py
def minimize_curvature(m: Manifold, anneal: bool = False):
    """ 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. """
    lib_py_gel.minimize_curvature(m.obj, anneal)

minimize_dihedral_angle

minimize_dihedral_angle(m: Manifold, max_iter: int = 10000, anneal: bool = False, alpha: bool = False, gamma: float = 4.0)

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.

Source code in pygel3d/hmesh.py
def minimize_dihedral_angle(m: Manifold, max_iter: int = 10000, anneal: bool = False, alpha: bool = False, gamma: float = 4.0):
    """ 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."""
    lib_py_gel.minimize_dihedral_angle(m.obj, max_iter, anneal,alpha,ct.c_double(gamma))

maximize_min_angle

maximize_min_angle(m: Manifold, dihedral_thresh: float = 0.95, anneal: bool = False)

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

Source code in pygel3d/hmesh.py
def maximize_min_angle(m: Manifold, dihedral_thresh: float = 0.95, anneal: bool = False):
    """ Maximizes the minimum angle of triangles by flipping edges of m. Makes the 
    mesh more Delaunay."""
    lib_py_gel.maximize_min_angle(m.obj,dihedral_thresh,anneal)

optimize_valency

optimize_valency(m: Manifold, anneal: bool = False)

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

Source code in pygel3d/hmesh.py
def optimize_valency(m: Manifold, anneal: bool = False):
    """ Tries to achieve valence 6 internally and 4 along edges by flipping edges 
    of m. """
    lib_py_gel.optimize_valency(m.obj, anneal)

randomize_mesh

randomize_mesh(m: Manifold, max_iter: int = 1)

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

Source code in pygel3d/hmesh.py
def randomize_mesh(m: Manifold, max_iter: int = 1):
    """  Make random flips in m. Useful for generating synthetic test cases. """
    lib_py_gel.randomize_mesh(m.obj, max_iter)

quadric_simplify

quadric_simplify(m: Manifold, keep_fraction: float, singular_thresh: float = 0.0001, error_thresh: float = 1)

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.

Source code in pygel3d/hmesh.py
def quadric_simplify(m: Manifold, keep_fraction: float, singular_thresh: float = 1e-4, error_thresh: float = 1):
    """ 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."""
    lib_py_gel.quadric_simplify(m.obj, keep_fraction, singular_thresh,error_thresh)

average_edge_length

average_edge_length(m: Manifold)

Returns the average edge length of mesh m.

Source code in pygel3d/hmesh.py
def average_edge_length(m: Manifold):
    """ Returns the average edge length of mesh m. """
    return lib_py_gel.average_edge_length(m.obj)

median_edge_length

median_edge_length(m: Manifold)

Returns the median edge length of m

Source code in pygel3d/hmesh.py
def median_edge_length(m: Manifold):
    """ Returns the median edge length of m"""
    return lib_py_gel.median_edge_length(m.obj)

refine_edges

refine_edges(m: Manifold, threshold: float)

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.

Source code in pygel3d/hmesh.py
def refine_edges(m: Manifold, threshold: float):
    """ 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."""
    return lib_py_gel.refine_edges(m.obj, threshold)

cc_split

cc_split(m: Manifold)

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.

Source code in pygel3d/hmesh.py
def cc_split(m: Manifold):
    """ 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."""
    lib_py_gel.cc_split(m.obj)

loop_split

loop_split(m: Manifold)

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

Source code in pygel3d/hmesh.py
def loop_split(m: Manifold):
    """ Perform a loop split on m where each edge is divided into two segments, and
    four new triangles are created for each original triangle. """
    lib_py_gel.loop_split(m.obj)

root3_subdivide

root3_subdivide(m: Manifold)

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

Source code in pygel3d/hmesh.py
def root3_subdivide(m: Manifold):
    """ Leif Kobbelt's subdivision scheme applied to m. A vertex is placed in the
    center of each face and all old edges are flipped. """
    lib_py_gel.root3_subdivide(m.obj)

rootCC_subdivide

rootCC_subdivide(m: Manifold)

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.

Source code in pygel3d/hmesh.py
def rootCC_subdivide(m: Manifold):
    """ 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. """
    lib_py_gel.rootCC_subdivide(m.obj)

butterfly_subdivide

butterfly_subdivide(m: Manifold)

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

Source code in pygel3d/hmesh.py
def butterfly_subdivide(m: Manifold):
    """ Butterfly subidiviosn on m. An interpolatory scheme. Creates the same connectivity as Loop. """
    lib_py_gel.butterfly_subdivide(m.obj)

cc_smooth

cc_smooth(m: Manifold, no_iters: int = 1)

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

Source code in pygel3d/hmesh.py
def cc_smooth(m: Manifold, no_iters: int = 1):
    """ If called after cc_split, this function completes a step of Catmull-Clark
    subdivision of m."""
    for _ in range(no_iters):
        lib_py_gel.cc_smooth(m.obj)

cc_subdivide

cc_subdivide(m: Manifold)

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

Source code in pygel3d/hmesh.py
def cc_subdivide(m: Manifold):
    """ Perform a full Catmull-Clark subdivision step on mesh m. """
    lib_py_gel.cc_split(m.obj)
    lib_py_gel.cc_smooth(m.obj)

loop_subdivide

loop_subdivide(m: Manifold)

Perform a full Loop subdivision step on mesh m.

Source code in pygel3d/hmesh.py
def loop_subdivide(m: Manifold):
    """ Perform a full Loop subdivision step on mesh m. """
    lib_py_gel.loop_split(m.obj)
    lib_py_gel.loop_smooth(m.obj)   

volume_preserving_cc_smooth

volume_preserving_cc_smooth(m: Manifold, no_iters: int = 1)

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.

Source code in pygel3d/hmesh.py
def volume_preserving_cc_smooth(m: Manifold, no_iters: int = 1):
    """ 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."""
    lib_py_gel.volume_preserving_cc_smooth(m.obj, no_iters)

regularize_quads

regularize_quads(m: Manifold, w: float = 0.5, shrink: float = 0.0, no_iters: int = 1)

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

Source code in pygel3d/hmesh.py
def regularize_quads(m: Manifold, w: float = 0.5, shrink: float = 0.0, no_iters: int = 1):
    """ This function smooths a quad mesh by regularizing quads. Essentially,
    regularization just makes them more rectangular. """
    lib_py_gel.regularize_quads(m.obj, w, shrink, no_iters)

loop_smooth

loop_smooth(m: Manifold)

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

Source code in pygel3d/hmesh.py
def loop_smooth(m: Manifold):
    """ If called after Loop split, this function completes a step of Loop
    subdivision of m. """
    lib_py_gel.loop_smooth(m.obj)

taubin_smooth

taubin_smooth(m: Manifold, no_iters: int = 1)

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

Source code in pygel3d/hmesh.py
def taubin_smooth(m: Manifold, no_iters: int = 1):
    """ This function performs Taubin smoothing on the mesh m for iter number
    of iterations. """
    lib_py_gel.taubin_smooth(m.obj, no_iters)

laplacian_smooth

laplacian_smooth(m: Manifold, w: float = 0.5, no_iters: int = 1)

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

Source code in pygel3d/hmesh.py
def laplacian_smooth(m: Manifold, w: float = 0.5, no_iters: int = 1):
    """ This function performs Laplacian smoothing on the mesh m for iter number
    of iterations. w is the weight applied. """
    lib_py_gel.laplacian_smooth(m.obj, w, no_iters)

anisotropic_smooth

anisotropic_smooth(m: Manifold, sharpness: float = 0.5, no_iters: int = 1)

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.

Source code in pygel3d/hmesh.py
def anisotropic_smooth(m: Manifold, sharpness: float = 0.5, no_iters: int = 1):
    """ 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."""
    lib_py_gel.anisotropic_smooth(m.obj, sharpness, no_iters)

volumetric_isocontour

volumetric_isocontour(data: ArrayLike, bbox_min: ArrayLike | None = None, bbox_max: ArrayLike | None = None, tau: float = 0.0, make_triangles: bool = True, high_is_inside: bool = True, dual_connectivity: bool = False) -> Manifold

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.

Source code in pygel3d/hmesh.py
def volumetric_isocontour(data: ArrayLike, 
                          bbox_min: ArrayLike | None = None, 
                          bbox_max: ArrayLike | None = None,
                          tau: float = 0.0,
                          make_triangles: bool = True,
                          high_is_inside: bool = True,
                          dual_connectivity: bool = False) -> Manifold:
    """ 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. """
    m = Manifold()
    dims = data.shape
    if bbox_min is None:
        bbox_min = (0,0,0)
    if bbox_max is None:
        bbox_max = dims
    data_float = np.asarray(data, dtype=ct.c_float, order='F')
    bbox_min_d = np.asarray(bbox_min, dtype=np.float64, order='C')
    bbox_max_d = np.asarray(bbox_max, dtype=np.float64, order='C')
    lib_py_gel.volumetric_isocontour(m.obj, dims[0], dims[1], dims[2],
                                     data_float, bbox_min_d, bbox_max_d, tau,
                                     make_triangles, high_is_inside, dual_connectivity)
    return m

triangulate

triangulate(m: Manifold, clip_ear: bool = True)

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

Source code in pygel3d/hmesh.py
def triangulate(m: Manifold, clip_ear: bool = True):
    """ Turn a general polygonal mesh, m, into a triangle mesh by repeatedly
        splitting a polygon into smaller polygons. """
    if clip_ear:
        lib_py_gel.ear_clip_triangulate(m.obj)
    else:
        lib_py_gel.shortest_edge_triangulate(m.obj)

extrude_faces

extrude_faces(m: Manifold, fset: ArrayLike | set[int]) -> set[int]

Inserts a new face loop around a set of faces given by fset.

Source code in pygel3d/hmesh.py
def extrude_faces(m: Manifold, fset: ArrayLike | set[int]) -> set[int]:
    """ Inserts a new face loop around a set of faces given by fset."""
    fvec = np.asarray(list(fset), dtype=ct.c_int)
    face_loop_out = IntVector()
    lib_py_gel.extrude_faces(m.obj,fvec,len(fvec), face_loop_out.obj)
    fset_out = set(face_loop_out)
    del face_loop_out
    return fset_out

kill_face_loop

kill_face_loop(m: Manifold)

Removes the face loop surrounding the patch of smallest area. This function has undefined effecto on a mesh that is not a pure quad mesh.

Source code in pygel3d/hmesh.py
def kill_face_loop(m: Manifold):
    """ Removes the face loop surrounding the patch of smallest area. This function has
    undefined effecto on a mesh that is not a pure quad mesh."""
    lib_py_gel.kill_face_loop(m.obj)

kill_degenerate_face_loops

kill_degenerate_face_loops(m: Manifold, thresh: float = 0.01)

Removes face loops which contain very poorly shaped faces. Must be called on a pure quad mesh.

Source code in pygel3d/hmesh.py
def kill_degenerate_face_loops(m: Manifold, thresh: float = 0.01):
    """ Removes face loops which contain very poorly shaped faces. Must be called on a pure
    quad mesh. """
    lib_py_gel.kill_degenerate_face_loops(m.obj, thresh)

graph_to_feq

graph_to_feq(g: Graph, node_radii: ArrayLike | float | None = None, symmetrize: bool = True) -> Manifold

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.

Source code in pygel3d/hmesh.py
def graph_to_feq(g: Graph, node_radii: ArrayLike | float | None = None, symmetrize: bool = True) -> Manifold:
    """ 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. """
    m = Manifold()
    if node_radii is None:
        node_radii = [0.0] * len(g.nodes())
        use_graph_radii = True
    else:
        use_graph_radii = False
        if isinstance(node_radii, (int, float)):
            if node_radii <= 0.0:
                node_radii = 0.25 * g.average_edge_length()
            node_radii = [node_radii] * len(g.nodes())

    node_rs_flat = np.asarray(node_radii, dtype=np.float64)
    lib_py_gel.graph_to_feq(g.obj , m.obj, node_rs_flat, symmetrize, use_graph_radii)
    return m

graph_to_cylinders

graph_to_cylinders(g: Graph, fudge: float = 0.0) -> Manifold

Creates a Manifold mesh from the graph. The first argument, g, is the graph we want converted, and fudge is a constant that is used to increase the radius of every node. This is useful if the radii are 0.

Source code in pygel3d/hmesh.py
def graph_to_cylinders(g: Graph, fudge: float = 0.0) -> Manifold:
    """ Creates a Manifold mesh from the graph. The first argument, g, is the
    graph we want converted, and fudge is a constant that is used to increase the radius
    of every node. This is useful if the radii are 0. """
    m = Manifold()
    lib_py_gel.graph_to_mesh_cyl(g.obj, m.obj, fudge)
    return m

graph_to_isosurface

graph_to_isosurface(g: Graph, fudge: float = 0.0, res: int = 256) -> Manifold

Creates a Manifold mesh from the graph. The first argument, g, is the graph we want converted, and fudge is a constant that is used to increase the radius of every node. This is useful if the radii are 0.

Source code in pygel3d/hmesh.py
def graph_to_isosurface(g: Graph, fudge: float = 0.0, res: int = 256) -> Manifold:
    """ Creates a Manifold mesh from the graph. The first argument, g, is the
    graph we want converted, and fudge is a constant that is used to increase the radius
    of every node. This is useful if the radii are 0. """
    m = Manifold()
    lib_py_gel.graph_to_mesh_iso(g.obj, m.obj, fudge, res)
    return m

fit_mesh_to_ref

fit_mesh_to_ref(m: Manifold, ref_mesh: Manifold, dist_wt: float = 0.5, lap_wt: float = 1.0, iter: int = 10)

Fits a skeletal mesh m to a reference mesh ref_mesh.

Source code in pygel3d/hmesh.py
def fit_mesh_to_ref(m: Manifold, ref_mesh: Manifold, dist_wt: float = 0.5, lap_wt: float = 1.0, iter: int = 10):
    """ Fits a skeletal mesh m to a reference mesh ref_mesh. """
    v_pos = m.positions()
    # ref_mesh = Manifold(m)
    # stable_marriage_registration(ref_mesh, _ref_mesh)
    # ref_pos = ref_mesh.positions()
    # A_list = []
    # b_list = []
    # N = len(m.vertices())
    # for vid in m.vertices():
    #     row_a = np.zeros(N)
    #     row_a[vid] = dist_wt
    #     A_list.append(row_a)
    #     b_list.append(ref_pos[vid]*dist_wt)
    # Ai, bi = csc_matrix(np.array(A_list)), np.array(b_list)
    for _ in range(iter):
        Ai, bi = _inv_correspondence_leqs(m, ref_mesh)
        lap_matrix = _laplacian_matrix(m)
        lap_b = lap_matrix @ v_pos
        final_A = vstack([lap_wt*lap_matrix, Ai])
        final_b = np.vstack([0*lap_b, bi])
        opt_x, _, _, _ = lsqr(final_A, final_b[:,0])[:4]
        opt_y, _, _, _ = lsqr(final_A, final_b[:,1])[:4]
        opt_z, _, _, _ = lsqr(final_A, final_b[:,2])[:4]
        v_pos[:,:] = np.stack([opt_x, opt_y, opt_z], axis=1)

rsr_recon

rsr_recon(vertices: ArrayLike, normals: ArrayLike = None, use_Euclid_dist: bool = False, genus: int = -1, num_neighbors: int = 70, max_neighbor_dist: float = 20, max_normal_ang: float = 60, max_handle_dist: int = 50) -> Manifold

RsR Reconstruction. The first argument, vertices, is the point cloud. The next argument, normals, are the normals associated with the vertices or empty list (default) if normals need to be estimated during reconstruction. use_Euclid_dist should be true if we can use the Euclidean rather than projected distance. Set to true only for noise free point clouds. genus controls handle insertion: -1 lets the algorithm detect genus, 0 disables handle insertion, and values > 0 request that many handles. num_neighbors is the number of nearest neighbors for each point, max_neighbor_dist is the maximum distance to farthest neighbor measured in multiples of average distance, max_normal_ang is the threshold on angles between normals: two points are only connected if the angle between their normals is less than max_normal_ang. Finally, max_handle_dist is the threshold on the distance between vertices that are connected by handle edges (check paper). For large max_handle_dist, it is harder for the algorithm to add handles.

Source code in pygel3d/hmesh.py
def rsr_recon(vertices: ArrayLike, 
              normals: ArrayLike=None, 
              use_Euclid_dist: bool=False, 
              genus: int=-1,
              num_neighbors: int=70,
              max_neighbor_dist: float=20,
              max_normal_ang: float=60,
              max_handle_dist: int=50) -> Manifold:
    """ RsR Reconstruction. The first argument, vertices, is the point cloud. The next argument,
        normals, are the normals associated with the vertices or empty list (default) if normals 
        need to be estimated during reconstruction. use_Euclid_dist should be true if we 
        can use the Euclidean rather than projected distance. Set to true only for noise free 
        point clouds. genus controls handle insertion: -1 lets the algorithm detect genus,
        0 disables handle insertion, and values > 0 request that many handles. num_neighbors is the number
        of nearest neighbors for each point,
        max_neighbor_dist is the maximum distance to farthest neighbor measured in multiples of average distance, 
        max_normal_ang is the threshold on angles between normals: two points are only connected if the angle
        between their normals is less than max_normal_ang. Finally, max_handle_dist is the threshold on the distance between 
        vertices that are connected by handle edges (check paper). For large max_handle_dist, it is harder for 
        the algorithm to add handles. """
    m = Manifold()
    vertices_data, n_vertices = _as_vec3_f(vertices)
    normal_data, n_normal = _as_vec3_f(normals)

    lib_py_gel.rsr_recon(m.obj, vertices_data, normal_data, n_vertices, n_normal, 
                         use_Euclid_dist, genus, num_neighbors, max_neighbor_dist, max_normal_ang, max_handle_dist)
    return m

hrsr_recon

hrsr_recon(vertices: ArrayLike, normals: ArrayLike = None, collapse_iters: int = 1, use_Euclid_dist: bool = False, genus: int = -1, num_neighbors: int = 70, max_neighbor_dist: float = 20, max_normal_ang: float = 60, max_handle_dist: int = 50, skip_reexpansion: bool = False) -> Manifold

Hierarchical RsR reconstruction. The arguments match rsr_recon, with two additions: collapse_iters controls how many collapse iterations to run, and skip_reexpansion disables the final reexpansion stage when set to True.

Genus semantics are intentionally the same as rsr_recon: - genus = -1: auto-detect - genus = 0: do not add handles - genus > 0: request genus handle insertions

Source code in pygel3d/hmesh.py
def hrsr_recon(vertices: ArrayLike,
               normals: ArrayLike=None,
               collapse_iters: int=1,
               use_Euclid_dist: bool=False,
               genus: int=-1,
               num_neighbors: int=70,
               max_neighbor_dist: float=20,
               max_normal_ang: float=60,
               max_handle_dist: int=50,
               skip_reexpansion: bool=False) -> Manifold:
    """ Hierarchical RsR reconstruction.
        The arguments match rsr_recon, with two additions: collapse_iters controls
        how many collapse iterations to run, and skip_reexpansion disables the
        final reexpansion stage when set to True.

        Genus semantics are intentionally the same as rsr_recon:
        - genus = -1: auto-detect
        - genus = 0: do not add handles
        - genus > 0: request genus handle insertions
    """
    m = Manifold()
    vertices_data, n_vertices = _as_vec3_f(vertices)
    normal_data, n_normal = _as_vec3_f(normals)

    lib_py_gel.hrsr_recon(m.obj, vertices_data, normal_data, n_vertices, n_normal,
                          collapse_iters, use_Euclid_dist, genus,
                          num_neighbors, max_neighbor_dist, max_normal_ang, max_handle_dist, skip_reexpansion)
    return m

connected_components

connected_components(m: Manifold) -> List[Manifold]

Returns a list of Manifolds that form the connected components of the mesh m.

Source code in pygel3d/hmesh.py
def connected_components(m: Manifold) -> List[Manifold]:
    """ Returns a list of Manifolds that form the connected components of the mesh m. """
    comp = lib_py_gel.connected_components(m.obj)
    N = lib_py_gel.mesh_vec_size(comp)
    if N == 0:
        return []
    meshes = []
    for i in range(N):
        obj = ct.c_void_p(lib_py_gel.mesh_vec_get(comp, i))
        meshes.append(Manifold(obj))
    lib_py_gel.mesh_vec_del(comp)
    return meshes

count_boundary_curves

count_boundary_curves(m: Manifold) -> int

Returns the number of boundary curves in the mesh m.

Source code in pygel3d/hmesh.py
def count_boundary_curves(m: Manifold) -> int:
    """ Returns the number of boundary curves in the mesh m. """
    return lib_py_gel.count_boundary_curves(m.obj)

analyze_topology

analyze_topology(m: Manifold) -> List[Dict[str, Any]]

Returns a list of dictionaries with information about the connected components of the mesh m. Each dictionary contains the Manifold ('m'), number of vertices ('V'), edges ('E'), faces ('F'), boundary curves ('b'), and the genus ('g') of the component. The genus is calculated using the Euler-Poincaré formula:

Source code in pygel3d/hmesh.py
def analyze_topology(m: Manifold) -> List[Dict[str, Any]]:
    """ Returns a list of dictionaries with information about the connected components of the mesh m.
    Each dictionary contains the Manifold ('m'), number of vertices ('V'), edges ('E'), faces ('F'), 
    boundary curves ('b'), and the genus ('g') of the component. The genus is calculated using the 
    Euler-Poincaré formula:"""
    components = connected_components(m)
    output = []
    for _,comp in enumerate(components):
        b = count_boundary_curves(comp)
        V = len(comp.vertices())  # Number of vertices
        E = len(comp.halfedges())//2  # Number of edges
        F = len(comp.faces())  # Number of faces
        g = -(V - E + F - 2 + b)//2 # Genus calculation
        output.append({'m': comp, 'V': V, 'E': E, 'F': F, 'b': b, 'g': g})
    return output

sphere_delaunay

sphere_delaunay(pts: ArrayLike) -> Manifold

Given a set of points on the unit sphere, compute the spherical Delaunay triangulation and return it as a Manifold mesh. The points should be given as an array-like of shape (N,3).

Source code in pygel3d/hmesh.py
def sphere_delaunay(pts: ArrayLike) -> Manifold:
    """ Given a set of points on the unit sphere, compute the spherical Delaunay triangulation
    and return it as a Manifold mesh. The points should be given as an array-like of shape (N,3). """
    m = Manifold()
    pts_data = np.asarray(pts, dtype=ct.c_double, order='C')
    if pts_data.size % 3 != 0:
        raise ValueError("pts should be of shape (N,3)")
    n_pts = pts_data.shape[0]
    lib_py_gel.sphere_delaunay(m.obj, pts_data, n_pts)
    return m

The hmesh module provides the core halfedge mesh data structure and associated operations for polygonal mesh processing.

Manifold Class

The Manifold class represents a polygonal mesh using the halfedge data structure, which enables efficient traversal and manipulation of mesh topology.

Creating Meshes

You can create meshes in several ways:

import pygel3d.hmesh as hmesh

# Create an empty mesh
m = hmesh.Manifold()

# Load from file
m = hmesh.load("model.obj")
m = hmesh.obj_load("model.obj")
m = hmesh.off_load("model.off")
m = hmesh.ply_load("model.ply")
m = hmesh.x3d_load("model.x3d")

Mesh I/O Functions

Loading Meshes

  • load(filename, mesh) - Load mesh from file (auto-detect format)
  • obj_load(filename, mesh) - Load Wavefront OBJ file
  • off_load(filename, mesh) - Load Object File Format
  • ply_load(filename, mesh) - Load PLY file
  • x3d_load(filename, mesh) - Load X3D file

Saving Meshes

  • obj_save(filename, mesh) - Save as Wavefront OBJ
  • off_save(filename, mesh) - Save as Object File Format
  • x3d_save(filename, mesh) - Save as X3D

Mesh Information

Basic Queries

  • valid(mesh) - Check if mesh is valid
  • closed(mesh) - Check if mesh is closed (no boundary)
  • bbox(mesh) - Get bounding box (returns min, max)
  • bsphere(mesh) - Get bounding sphere (returns center, radius)

Counts

  • mesh.no_vertices() - Number of vertices
  • mesh.no_faces() - Number of faces
  • mesh.no_halfedges() - Number of halfedges
  • count_boundary_curves(mesh) - Count boundary loops

Mesh Processing

Cleaning and Repair

  • stitch_mesh(mesh, threshold) - Merge nearby vertices
  • close_holes(mesh, max_size) - Fill holes up to max_size edges
  • remove_caps(mesh, threshold) - Remove cap-like features
  • remove_needles(mesh, threshold, avg_pos) - Remove needle-like features
  • merge_coincident_boundary_vertices(mesh, threshold) - Merge boundary vertices

Smoothing

  • cc_smooth(mesh) - Catmull-Clark smoothing
  • loop_smooth(mesh) - Loop smoothing
  • taubin_smooth(mesh, iterations) - Taubin smoothing
  • laplacian_smooth(mesh, weight, iterations) - Laplacian smoothing
  • anisotropic_smooth(mesh, sharpness, iterations) - Anisotropic smoothing

Subdivision

  • cc_split(mesh) - Catmull-Clark subdivision
  • loop_split(mesh) - Loop subdivision (for triangles)
  • root3_subdivide(mesh) - Root-3 subdivision
  • butterfly_subdivide(mesh) - Butterfly subdivision

Simplification

  • quadric_simplify(mesh, keep_fraction, singular_threshold, error_threshold) - Quadric error metric simplification

Refinement

  • refine_edges(mesh, threshold) - Refine long edges

Triangulation

  • shortest_edge_triangulate(mesh) - Triangulate using shortest diagonal
  • ear_clip_triangulate(mesh) - Triangulate using ear clipping

Optimization

  • minimize_curvature(mesh, anneal) - Minimize mesh curvature
  • minimize_dihedral_angle(mesh, max_iter, anneal, alpha, gamma) - Minimize dihedral angles
  • maximize_min_angle(mesh, threshold, anneal) - Maximize minimum angle
  • optimize_valency(mesh, anneal) - Optimize vertex valency
  • randomize_mesh(mesh, max_iter) - Random edge flips

Topology Operations

  • flip_orientation(mesh) - Reverse face orientation
  • cc_split(mesh) - Catmull-Clark split

Mesh Measurements

Geometric Measurements

  • area(mesh, face_id) - Area of a face
  • perimeter(mesh, face_id) - Perimeter of a face
  • length(mesh, halfedge_id) - Length of an edge
  • total_area(mesh) - Total surface area
  • volume(mesh) - Mesh volume

Vertex Measurements

  • valency(mesh, vertex_id) - Vertex valency (degree)
  • one_ring_area(mesh, vertex_id) - Area of one-ring neighborhood
  • mixed_area(mesh, vertex_id) - Mixed Voronoi/barycentric area

Curvature

  • gaussian_curvature(mesh, vertex_id) - Gaussian curvature at vertex
  • mean_curvature(mesh, vertex_id) - Mean curvature at vertex
  • principal_curvatures(mesh, vertex_id) - Principal curvatures

Normals

  • vertex_normal(mesh, vertex_id) - Vertex normal
  • face_normal(mesh, face_id) - Face normal

Mesh Traversal

Vertex Operations

  • mesh.vertices() - Iterator over all vertices
  • mesh.circulate_vertex(vertex_id, mode) - Circulate around vertex

Face Operations

  • mesh.faces() - Iterator over all faces
  • mesh.circulate_face(face_id, mode) - Circulate around face
  • no_edges(mesh, face_id) - Number of edges in face
  • centre(mesh, face_id) - Face center

Halfedge Operations

  • mesh.halfedges() - Iterator over all halfedges

Mesh Editing

Adding Elements

  • mesh.add_vertex(position) - Add a vertex
  • mesh.add_face(positions) - Add a face

Removing Elements

  • mesh.remove_vertex(vertex_id) - Remove a vertex
  • mesh.remove_face(face_id) - Remove a face
  • mesh.remove_edge(halfedge_id) - Remove an edge

Modifying Topology

  • mesh.flip_edge(halfedge_id) - Flip an edge
  • mesh.collapse_edge(halfedge_id, avg_vertices) - Collapse an edge
  • mesh.split_edge(halfedge_id) - Split an edge
  • mesh.split_face_by_edge(face_id, v0, v1) - Split face with new edge
  • mesh.split_face_by_vertex(face_id) - Split face from center
  • mesh.merge_faces(face_id, halfedge_id) - Merge two faces
  • mesh.stitch_boundary_edges(h0, h1) - Stitch two boundary edges
  • mesh.close_hole(halfedge_id) - Close a boundary loop

Status Queries

  • mesh.vertex_in_use(vertex_id) - Check if vertex exists
  • mesh.face_in_use(face_id) - Check if face exists
  • mesh.halfedge_in_use(halfedge_id) - Check if halfedge exists

Boundary Queries

  • is_vertex_at_boundary(mesh, vertex_id) - Check if vertex is on boundary
  • is_halfedge_at_boundary(mesh, halfedge_id) - Check if halfedge is on boundary
  • boundary_edge(mesh, vertex_id, halfedge_id) - Check if edge is on boundary

Connectivity

  • connected(mesh, v0, v1) - Check if two vertices are connected

Walker Functions

Walker functions provide low-level halfedge traversal:

  • mesh.walker.next_halfedge(h) - Get next halfedge in face
  • mesh.walker.prev_halfedge(h) - Get previous halfedge in face
  • mesh.walker.opposite_halfedge(h) - Get opposite halfedge
  • mesh.walker.incident_face(h) - Get incident face
  • mesh.walker.incident_vertex(h) - Get incident vertex

Advanced Operations

Volumetric Operations

  • volumetric_isocontour(mesh, x_dim, y_dim, z_dim, data, pmin, pmax, tau, make_triangles, high_is_inside, dual_connectivity) - Extract isosurface from volume

Registration

  • non_rigid_registration(mesh, reference_mesh) - Non-rigid registration
  • stable_marriage_registration(mesh, reference_mesh) - Stable marriage registration

Reconstruction

  • rsr_recon(mesh, vertices, normals, v_num, n_num, isEuclidean, genus, k, r, theta, n) - Rotation system reconstruction

Face Operations

  • extrude_faces(mesh, face_list, output_face_list) - Extrude faces
  • kill_face_loop(mesh) - Remove face loop
  • kill_degenerate_face_loops(mesh, threshold) - Remove degenerate loops

Connected Components

  • connected_components(mesh) - Split into connected components

MeshDistance Class

The MeshDistance class provides efficient distance queries to triangle meshes.

from pygel3d import MeshDistance
import pygel3d.hmesh as hmesh

# Load mesh
m = hmesh.load("model.obj")

# Create distance object
dist = MeshDistance(m)

# Query signed distance
point = [0, 0, 0]
distance = dist.signed_distance(point)

# Query unsigned distance
distance = dist.distance(point)

Example Usage

Complete Mesh Processing Pipeline

import pygel3d.hmesh as hmesh

# Load mesh
m = hmesh.load("input.obj")

# Clean mesh
hmesh.stitch_mesh(m, 1e-6)
hmesh.close_holes(m, 100)
hmesh.remove_caps(m, 0.1)

# Smooth
hmesh.cc_smooth(m)

# Triangulate
hmesh.shortest_edge_triangulate(m)

# Simplify
hmesh.quadric_simplify(m, keep_fraction=0.5)

# Optimize
hmesh.minimize_curvature(m, anneal=True)

# Save
hmesh.obj_save("output.obj", m)