Module Vertex

Expand source code
# Copyright (C) 2024
# Wassim Jabi <wassim.jabi@gmail.com>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/>.

import topologic_core as topologic
import collections
import os
import warnings

try:
    import numpy as np
except:
    print("Vertex - Installing required numpy library.")
    try:
        os.system("pip install numpy")
    except:
        os.system("pip install numpy --user")
    try:
        import numpy as np
        print("Vertex - numpy library installed successfully.")
    except:
        warnings.warn("Vertex - Error: Could not import numpy.")

class Vertex():
    @staticmethod
    def AreCollinear(vertices: list, tolerance: float = 0.0001):
        """
        Returns True if the input list of vertices form a straight line. Returns False otherwise.

        Parameters
        ----------
        vertices : list
            The input list of vertices.
        tolerance : float, optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise.

        """
        from topologicpy.Cluster import Cluster
        from topologicpy.Topology import Topology
        from topologicpy.Vector import Vector
        import sys
        def areCollinear(vertices, tolerance=0.0001):
            point1 = [Vertex.X(vertices[0]), Vertex.Y(vertices[0]), Vertex.Z(vertices[0])]
            point2 = [Vertex.X(vertices[1]), Vertex.Y(vertices[1]), Vertex.Z(vertices[1])]
            point3 = [Vertex.X(vertices[2]), Vertex.Y(vertices[2]), Vertex.Z(vertices[2])]

            vector1 = [point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]]
            vector2 = [point3[0] - point1[0], point3[1] - point1[1], point3[2] - point1[2]]

            cross_product_result = Vector.Cross(vector1, vector2, tolerance=tolerance)
            return cross_product_result == None
        
        if not isinstance(vertices, list):
            print("Vertex.AreCollinear - Error: The input list of vertices is not a valid list. Returning None.")
            return None
        vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
        if len(vertexList) < 2:
            print("Vertex.AreCollinear - Error: The input list of vertices does not contain sufficient valid vertices. Returning None.")
            return None
        if len(vertexList) < 3:
            return True # Any two vertices can form a line!
        cluster = Topology.SelfMerge(Cluster.ByTopologies(vertexList), tolerance=tolerance)
        vertexList = Topology.Vertices(cluster)
        slices = []
        for i in range(2,len(vertexList)):
            slices.append([vertexList[0], vertexList[1], vertexList[i]])
        for slice in slices:
            if not areCollinear(slice, tolerance=tolerance):
                return False
        return True
    
    @staticmethod
    def AreIpsilateral(vertices: list, face) -> bool:
        """
        Returns True if the input list of vertices are on one side of a face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        vertices : list
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        def check(dot_productA, pointB, pointC, normal):
            # Calculate the dot products of the vectors from the surface point to each of the input points.
            dot_productB = (pointB[0] - pointC[0]) * normal[0] + \
                        (pointB[1] - pointC[1]) * normal[1] + \
                        (pointB[2] - pointC[2]) * normal[2]

            # Check if both points are on the same side of the surface.
            if dot_productA * dot_productB > 0:
                return True

            # Check if both points are on opposite sides of the surface.
            elif dot_productA * dot_productB < 0:
                return False

            # Otherwise, at least one point is on the surface.
            else:
                return True
    
        from topologicpy.Vertex import Vertex
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(face, "Face"):
            return None
        vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
        if len(vertexList) < 2:
            return None
        pointA = Vertex.Coordinates(vertexList[0])
        pointC = Vertex.Coordinates(Face.VertexByParameters(face, 0.5, 0.5))
        normal = Face.Normal(face)
        dot_productA = (pointA[0] - pointC[0]) * normal[0] + \
                        (pointA[1] - pointC[1]) * normal[1] + \
                        (pointA[2] - pointC[2]) * normal[2]
        for i in range(1, len(vertexList)):
            pointB = Vertex.Coordinates(vertexList[i])
            if not check(dot_productA, pointB, pointC, normal):
                return False
        return True
    
    @staticmethod
    def AreIpsilateralCluster(cluster, face) -> bool:
        """
        Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(cluster, "Topology"):
            return None
        vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
        return Vertex.AreIpsilateral(vertices, face)
    
    @staticmethod
    def AreOnSameSide(vertices: list, face) -> bool:
        """
        Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        vertices : list
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        return Vertex.AreIpsilateral(vertices, face)

    @staticmethod
    def AreOnSameSideCluster(cluster, face) -> bool:
        """
        Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(cluster, "Topology"):
            return None
        vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
        return Vertex.AreIpsilateral(vertices, face)

    @staticmethod
    def ByCoordinates(*args, **kwargs):
        """
        Creates a vertex at the coordinates specified by the x, y, z inputs. You can call this method using a list of coordinates or individually.
        Examples:
        v = Vertex.ByCoordinates(3.4, 5.7, 2.8)
        v = Vertex.ByCoordinates([3.4, 5.7, 2.8])
        v = Vertex.ByCoordinates(x=3.4, y=5.7, z=2.8)

        Parameters
        ----------
        x : float , optional
            The X coordinate. The default is 0.
        y : float , optional
            The Y coordinate. The default is 0.
        z : float , optional
            The Z coordinate. The defaults is 0.

        Returns
        -------
        topologic_core.Vertex
            The created vertex.

        """
        import numbers
        x = None
        y = None
        z = None
        if len(args) > 3 or len(kwargs.items()) > 3:
            print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
            return None
        if len(args) > 0:
            value = args[0]
            if isinstance(value, list) and len(value) > 3:
                print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
                return None
            elif isinstance(value, list) and len(value) == 3:
                x = value[0]
                y = value[1]
                z = value[2]
            elif isinstance(value, list) and len(value) == 2:
                x = value[0]
                y = value[1]
            elif isinstance(value, list) and len(value) == 1:
                x = value[0]
            elif len(args) == 3:
                x = args[0]
                y = args[1]
                z = args[2]
            elif len(args) == 2:
                x = args[0]
                y = args[1]
            elif len(args) == 1:
                x = args[0]
        for key, value in kwargs.items():
            if "x" in key.lower():
                if not x == None:
                    print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                    return None
                x = value
            elif "y" in key.lower():
                if not y == None:
                    print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                    return None
                y = value
            elif "z" in key.lower():
                if not z == None:
                    print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                    return None
                z = value
        if x == None:
            x = 0
        if y == None:
            y = 0
        if z == None:
            z = 0
        if not isinstance(x, numbers.Number):
            print("Vertex.ByCoordinates - Error: The x value is not a valid number. Returning None.")
            return None
        if not isinstance(y, numbers.Number):
            print("Vertex.ByCoordinates - Error: The y value is not a valid number. Returning None.")
            return None
        if not isinstance(z, numbers.Number):
            print("Vertex.ByCoordinates - Error: The z value is not a valid number. Returning None.")
            return None
        
        vertex = None
        try:
            vertex = topologic.Vertex.ByCoordinates(x, y, z) # Hook to Core
        except:
            vertex = None
            print("Vertex.ByCoordinates - Error: Could not create a topologic vertex. Returning None.")
        return vertex
    
    @staticmethod
    def Centroid(vertices):
        """
        Returns the centroid of the input list of vertices.

        Parameters
        -----------
        vertices : list
            The input list of vertices

        Return
        ----------
        topologic_core.Vertex
            The computed centroid of the input list of vertices
        
        """
        from topologicpy.Topology import Topology

        if not isinstance(vertices, list):
            print("Vertex.Centroid - Error: The input vertices parameter is not a valid list. Returning None.")
            return None
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) < 1:
            print("Vertex.Centroid - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
            return None
        if len(vertices) == 1:
            return vertices[0]
        cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
        cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)
        cz = sum(Vertex.Z(v) for v in vertices) / len(vertices)
        return Vertex.ByCoordinates(cx, cy, cz)
    
    @staticmethod
    def Clockwise2D(vertices):
        """
        Sorts the input list of vertices in a clockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

        Parameters
        -----------
        vertices : list
            The input list of vertices

        Return
        -----------
        list
            The input list of vertices sorted in a counter clockwise fashion
        
        """
        return list(reversed(Vertex.CounterClockwise2D(vertices)))
    
    @staticmethod
    def Coordinates(vertex, outputType: str = "xyz", mantissa: int = 6) -> list:
        """
        Returns the coordinates of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        outputType : string, optional
            The desired output type. Could be any permutation or substring of "xyz" or the string "matrix". The default is "xyz". The input is case insensitive and the coordinates will be returned in the specified order.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        list
            The coordinates of the input vertex.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        x = round(vertex.X(), mantissa)
        y = round(vertex.Y(), mantissa)
        z = round(vertex.Z(), mantissa)
        matrix = [[1, 0, 0, x],
                [0, 1, 0, y],
                [0, 0, 1, z],
                [0, 0, 0, 1]]
        output = []
        outputType = outputType.lower()
        if outputType == "matrix":
            return matrix
        else:
            outputType = list(outputType)
            for axis in outputType:
                if axis == "x":
                    output.append(x)
                elif axis == "y":
                    output.append(y)
                elif axis == "z":
                    output.append(z)
        return output

    @staticmethod
    def CounterClockwise2D(vertices):
        """
        Sorts the input list of vertices in a counterclockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

        Parameters
        -----------
        vertices : list
            The input list of vertices

        Return
        -----------
        list
            The input list of vertices sorted in a counter clockwise fashion
        
        """
        import math
        # find the centroid of the points
        cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
        cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)

        # sort the points based on their angle with respect to the centroid
        vertices.sort(key=lambda v: (math.atan2(Vertex.Y(v) - cy, Vertex.X(v) - cx) + 2 * math.pi) % (2 * math.pi))
        return vertices

    @staticmethod
    def Degree(vertex, hostTopology, topologyType: str = "edge"):
        """
        Returns the vertex degree (the number of super topologies connected to it). See https://en.wikipedia.org/wiki/Degree_(graph_theory).

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        hostTopology : topologic_core.Topology
            The input host topology in which to search for the connected super topologies.
        topologyType : str , optional
            The topology type to search for. This can be any of "edge", "wire", "face", "shell", "cell", "cellcomplex", "cluster". It is case insensitive. If set to None, the immediate supertopology type is searched for. The default is None.

        Returns
        -------
        int
            The number of super topologies connected to this vertex

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            print("Vertex.Degree - Error: The input vertex parameter is not a valid topologic vertex. Returning None.")
        if not Topology.IsInstance(hostTopology, "Topology"):
            print("Vertex.Degree - Error: The input hostTopology parameter is not a valid topologic topology. Returning None.")
        superTopologies = Topology.SuperTopologies(topology=vertex, hostTopology=hostTopology, topologyType=topologyType)
        return len(superTopologies)


    @staticmethod
    def Distance(vertex, topology, includeCentroid: bool =True,
                 mantissa: int = 6) -> float:
        """
        Returns the distance between the input vertex and the input topology. This method returns the distance to the closest sub-topology in the input topology, optionally including its centroid.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        includeCentroid : bool
            If set to True, the centroid of the input topology will be considered in finding the nearest subTopology to the input vertex. The default is True.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.
        
        Returns
        -------
        float
            The distance between the input vertex and the input topology.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology
        import math

        def distance_point_to_point(point1, point2):
            # Convert input points to NumPy arrays
            point1 = np.array(point1)
            point2 = np.array(point2)
            
            # Calculate the Euclidean distance
            distance = np.linalg.norm(point1 - point2)
            
            return distance

        def distance_point_to_line(point, line_start, line_end):
            # Convert input points to NumPy arrays for vector operations
            point = np.array(point)
            line_start = np.array(line_start)
            line_end = np.array(line_end)
            
            # Calculate the direction vector of the edge
            line_direction = line_end - line_start
            
            # Vector from the edge's starting point to the point
            point_to_start = point - line_start
            
            # Calculate the parameter 't' where the projection of the point onto the edge occurs
            if np.dot(line_direction, line_direction) == 0:
                t = 0
            else:
                t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
            
            # Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
            if t < 0:
                return np.linalg.norm(point - line_start)
            elif t > 1:
                return np.linalg.norm(point - line_end)
            
            # Calculate the closest point on the edge to the given point
            closest_point = line_start + t * line_direction
            
            # Calculate the distance between the closest point and the given point
            distance = np.linalg.norm(point - closest_point)
            
            return distance
        
        def distance_to_vertex(vertexA, vertexB):
            a = (Vertex.X(vertexA), Vertex.Y(vertexA), Vertex.Z(vertexA))
            b = (Vertex.X(vertexB), Vertex.Y(vertexB), Vertex.Z(vertexB))
            return distance_point_to_point(a, b)
        
        def distance_to_edge(vertex, edge):
            a = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
            sv = Edge.StartVertex(edge)
            ev = Edge.EndVertex(edge)
            svp = (Vertex.X(sv), Vertex.Y(sv), Vertex.Z(sv))
            evp = (Vertex.X(ev), Vertex.Y(ev), Vertex.Z(ev))
            return distance_point_to_line(a,svp, evp)
        
        def distance_to_face(vertex, face, includeCentroid):
            v_proj = Vertex.Project(vertex, face, mantissa=mantissa)
            if not Vertex.IsInternal(v_proj, face):
                vertices = Topology.Vertices(topology)
                distances = [distance_to_vertex(vertex, v) for v in vertices]
                edges = Topology.Edges(topology)
                distances += [distance_to_edge(vertex, e) for e in edges]
                if includeCentroid:
                    distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
                return min(distances)
            dic = Face.PlaneEquation(face)
            a = dic["a"]
            b = dic["b"]
            c = dic["c"]
            d = dic["d"]
            x1, y1, z1 = Vertex.Coordinates(vertex)
            d = abs((a * x1 + b * y1 + c * z1 + d))
            e = (math.sqrt(a * a + b * b + c * c))
            if e == 0:
                return 0
            return d/e
        if not Topology.IsInstance(vertex, "Vertex") or not Topology.IsInstance(topology, "Topology"):
            return None
        if Topology.IsInstance(topology, "Vertex"):
            return round(distance_to_vertex(vertex,topology), mantissa)
        elif Topology.IsInstance(topology, "Edge"):
            return round(distance_to_edge(vertex,topology), mantissa)
        elif Topology.IsInstance(topology, "Wire"):
            vertices = Topology.Vertices(topology)
            distances = [distance_to_vertex(vertex, v) for v in vertices]
            edges = Topology.Edges(topology)
            distances += [distance_to_edge(vertex, e) for e in edges]
            if includeCentroid:
                distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
            return round(min(distances), mantissa)
        elif Topology.IsInstance(topology, "Face"):
            vertices = Topology.Vertices(topology)
            distances = [distance_to_vertex(vertex, v) for v in vertices]
            edges = Topology.Edges(topology)
            distances += [distance_to_edge(vertex, e) for e in edges]
            distances.append(distance_to_face(vertex,topology, includeCentroid))
            if includeCentroid:
                distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
            return round(min(distances), mantissa)
        elif Topology.IsInstance(topology, "Shell") or Topology.IsInstance(topology, "Cell") or Topology.IsInstance(topology, "CellComplex") or Topology.IsInstance(topology, "Cluster"):
            vertices = Topology.Vertices(topology)
            distances = [distance_to_vertex(vertex, v) for v in vertices]
            edges = Topology.Edges(topology)
            distances += [distance_to_edge(vertex, e) for e in edges]
            faces = Topology.Faces(topology)
            distances += [distance_to_face(vertex, f, includeCentroid) for f in faces]
            if includeCentroid:
                distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
            return round(min(distances), mantissa)
        else:
            print("Vertex.Distance - Error: Could not recognize the input topology. Returning None.")
            return None
    
    @staticmethod
    def EnclosingCell(vertex, topology, exclusive: bool = True, tolerance: float = 0.0001) -> list:
        """
        Returns the list of Cells found in the input topology that enclose the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        exclusive : bool , optional
            If set to True, return only the first found enclosing cell. The default is True.
        tolerance : float , optional
            The tolerance for computing if the input vertex is enclosed in a cell. The default is 0.0001.

        Returns
        -------
        list
            The list of enclosing cells.

        """
        from topologicpy.Cell import Cell
        from topologicpy.Topology import Topology
        
        def boundingBox(cell):
            vertices = []
            _ = cell.Vertices(None, vertices)
            x = []
            y = []
            z = []
            for aVertex in vertices:
                x.append(aVertex.X())
                y.append(aVertex.Y())
                z.append(aVertex.Z())
            return ([min(x), min(y), min(z), max(x), max(y), max(z)])
        
        if Topology.IsInstance(topology, "Cell"):
            cells = [topology]
        elif Topology.IsInstance(topology, "Cluster") or Topology.IsInstance(topology, "CellComplex"):
            cells = []
            _ = topology.Cells(None, cells)
        else:
            return None
        if len(cells) < 1:
            return None
        enclosingCells = []
        for i in range(len(cells)):
            bbox = boundingBox(cells[i])
            if ((vertex.X() < bbox[0]) or (vertex.Y() < bbox[1]) or (vertex.Z() < bbox[2]) or (vertex.X() > bbox[3]) or (vertex.Y() > bbox[4]) or (vertex.Z() > bbox[5])) == False:
                if Vertex.IsInternal(vertex, cells[i], tolerance=tolerance):
                    if exclusive:
                        return([cells[i]])
                    else:
                        enclosingCells.append(cells[i])
        return enclosingCells

    @staticmethod
    def Fuse(vertices: list, mantissa: int = 6, tolerance: float = 0.0001):
        """
        Returns a list of vertices where vertices within a specified tolerance distance are fused while retaining duplicates, ensuring that vertices with nearly identical coordinates are replaced by a single shared coordinate.

        Parameters
        ----------
        vertices : list
            The input list of topologic vertices.
        mantissa : int , optional
            The desired length of the mantissa for retrieving vertex coordinates. The default is 6.
        tolerance : float , optional
            The desired tolerance for computing if vertices need to be fused. Any vertices that are closer to each other than this tolerance will be fused. The default is 0.0001.

         Returns
        -------
        list
            The list of fused vertices. This list contains the same number of vertices and in the same order as the input list of vertices. However, the coordinates
            of these vertices have now been modified so that they are exactly the same with other vertices that are within the tolerance distance.
        """
        from topologicpy.Topology import Topology
        import numpy as np

        def fuse_vertices(vertices, tolerance):
            fused_vertices = []
            merged_indices = {}

            for idx, vertex in enumerate(vertices):
                if idx in merged_indices:
                    fused_vertices.append(fused_vertices[merged_indices[idx]])
                    continue

                merged_indices[idx] = len(fused_vertices)
                fused_vertex = vertex
                for i in range(idx + 1, len(vertices)):
                    if i in merged_indices:
                        continue

                    other_vertex = vertices[i]
                    distance = np.linalg.norm(np.array(vertex) - np.array(other_vertex))
                    if distance < tolerance:
                        # Choose the coordinate with the least amount of decimal points
                        if count_decimal_points(other_vertex) < count_decimal_points(fused_vertex):
                            fused_vertex = other_vertex

                        merged_indices[i] = len(fused_vertices)

                fused_vertices.append(fused_vertex)

            return fused_vertices
        def count_decimal_points(vertex):
            # Count the number of decimal points in the coordinates
            decimals_list = []
            for coord in vertex:
                coord_str = str(coord)
                if '.' in coord_str:
                    decimals_list.append(len(coord_str.split('.')[1]))
                elif 'e' in coord_str:
                    decimals_list.append(int(coord_str.split('e')[1].replace('-','')))
            return max(decimals_list)


        if not isinstance(vertices, list):
            print("Vertex.Fuse - Error: The input vertices parameter is not a valid list. Returning None.")
            return None
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) == 0:
            print("Vertex.Fuse - Error: The input vertices parameter does not contain any valid topologic vertices. Returning None.")
            return None
        
        vertices = [(Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa), Vertex.Z(v, mantissa=mantissa)) for v in vertices]
        fused_vertices = fuse_vertices(vertices, tolerance)
        return_vertices = [Vertex.ByCoordinates(list(coord)) for coord in fused_vertices]
        return return_vertices

    @staticmethod
    def Index(vertex, vertices: list, strict: bool = False, tolerance: float = 0.0001) -> int:
        """
        Returns index of the input vertex in the input list of vertices

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        vertices : list
            The input list of vertices.
        strict : bool , optional
            If set to True, the vertex must be strictly identical to the one found in the list. Otherwise, a distance comparison is used. The default is False.
        tolerance : float , optional
            The tolerance for computing if the input vertex is identical to a vertex from the list. The default is 0.0001.

        Returns
        -------
        int
            The index of the input vertex in the input list of vertices.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        if not isinstance(vertices, list):
            return None
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) == 0:
            return None
        for i in range(len(vertices)):
            if strict:
                if Topology.IsSame(vertex, vertices[i]):
                    return i
            else:
                d = Vertex.Distance(vertex, vertices[i])
                if d < tolerance:
                    return i
        return None

    @staticmethod
    def InterpolateValue(vertex, vertices, n=3, key="intensity", tolerance=0.0001):
        """
        Interpolates the value of the input vertex based on the values of the *n* nearest vertices.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        vertices : list
            The input list of vertices.
        n : int , optional
            The maximum number of nearest vertices to consider. The default is 3.
        key : str , optional
            The key that holds the value to be interpolated in the dictionaries of the vertices. The default is "intensity".
        tolerance : float , optional
            The tolerance for computing if the input vertex is coincident with another vertex in the input list of vertices. The default is 0.0001.

        Returns
        -------
        topologic_core.vertex
            The input vertex with the interpolated value stored in its dictionary at the key specified by the input key. Other keys and values in the dictionary are preserved.

        """

        def interpolate_value(point, data_points, n, tolerance=0.0001):
            """
            Interpolates the value associated with a point in 3D by averaging the values of the n nearest points.
            The influence of the adjacent points is inversely proportional to their distance from the input point.

            Args:
                data_points (list): A list of tuples, each representing a data point in 3D space as (x, y, z, value).
                                    The 'value' represents the value associated with that data point.
                point (tuple): A tuple representing the point in 3D space as (x, y, z) for which we want to interpolate a value.
                n (int): The number of nearest points to consider for interpolation.

            Returns:
                The interpolated value for the input point.
            """
            # Calculate the distances between the input point and all data points
            distances = [(distance(p[:3], point), p[3]) for p in data_points]

            # Sort the distances in ascending order
            sorted_distances = sorted(distances, key=lambda x: x[0])

            # Take the n nearest points
            nearest_points = sorted_distances[:n]

            n_p = nearest_points[0]
            n_d = n_p[0]
            if n_d < tolerance:
                return n_p[1]

            # Calculate the weights for each nearest point based on inverse distance

            weights = [(1/d[0], d[1]) for d in nearest_points]

            # Normalize the weights so they sum to 1
            total_weight = sum(w[0] for w in weights)
            normalized_weights = [(w[0]/total_weight, w[1]) for w in weights]

            # Interpolate the value as the weighted average of the nearest points
            interpolated_value = sum(w[0]*w[1] for w in normalized_weights)

            return interpolated_value

        def distance(point1, point2):
            """
            Calculates the Euclidean distance between two points in 3D space.

            Args:
                point1 (tuple): A tuple representing a point in 3D space as (x, y, z).
                point2 (tuple): A tuple representing a point in 3D space as (x, y, z).

            Returns:
                The Euclidean distance between the two points.
            """
            return ((point1[0]-point2[0])**2 + (point1[1]-point2[1])**2 + (point1[2]-point2[2])**2)**0.5
        
        from topologicpy.Topology import Topology
        from topologicpy.Dictionary import Dictionary

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        if not isinstance(vertices, list):
            return None
        
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) == 0:
            return None
        
        point = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
        data_points = []
        for v in vertices:
            d = Topology.Dictionary(v)
            value = Dictionary.ValueAtKey(d, key)
            if not value == None:
                if type(value) == int or type(value) == float:
                    data_points.append((Vertex.X(v), Vertex.Y(v), Vertex.Z(v), value))
        if len(data_points) == 0:
            return None
        if n > len(data_points):
            n = len(data_points)
        value = interpolate_value(point, data_points, n, tolerance=0.0001)
        d = Topology.Dictionary(vertex)
        d = Dictionary.SetValueAtKey(d, key, value)
        vertex = Topology.SetDictionary(vertex, d)
        return vertex

    @staticmethod
    def IsCoincident(vertexA, vertexB, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertexA is coincident with the input vertexB. Returns False otherwise.

        Parameters
        ----------
        vertexA : topologic_core.Vertex
            The first input vertex.
        vertexB : topologic_core.Vertex
            The second input vertex.
        tolerance : float , optional
            The tolerance for computing if the input vertexA is coincident with the input vertexB. The default is 0.0001.

        Returns
        -------
        bool
            True if the input vertexA is coincident with the input vertexB. False otherwise.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertexA, "Vertex"):
            if not silent:
                print("Vertex.IsCoincident - Error: The input vertexA parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(vertexB, "Vertex"):
            if not silent:
                print("Vertex.IsICoincident - Error: The input vertexB parameter is not a valid vertex. Returning None.")
            return None
        return Vertex.IsInternal(vertexA, vertexB, tolerance=tolerance, silent=silent)

    @staticmethod
    def IsExternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertex is external to the input topology. Returns False otherwise.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        tolerance : float , optional
            The tolerance for computing if the input vertex is external to the input topology. The default is 0.0001.
        silent : bool , optional
            If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

        Returns
        -------
        bool
            True if the input vertex is external to the input topology. False otherwise.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            if not silent:
                print("Vertex.IsExternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(topology, "Topology"):
            if not silent:
                print("Vertex.IsExternal - Error: The input topology parameter is not a valid topology. Returning None.")
            return None
        return not (Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent) or Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent))
    
    @staticmethod
    def IsInternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertex is inside the input topology. Returns False otherwise.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        tolerance : float , optional
            The tolerance for computing if the input vertex is internal to the input topology. The default is 0.0001.
        silent : bool , optional
            If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

        Returns
        -------
        bool
            True if the input vertex is internal to the input topology. False otherwise.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Wire import Wire
        from topologicpy.Face import Face
        from topologicpy.CellComplex import CellComplex
        from topologicpy.Cluster import Cluster
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            if not silent:
                print("Vertex.IsInternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(topology, "Topology"):
            if not silent:
                print("Vertex.IsInternal - Error: The input topology parameter is not a valid topology. Returning None.")
            return None

        if Topology.IsInstance(topology, "Vertex"):
            return Vertex.Distance(vertex, topology) < tolerance
        elif Topology.IsInstance(topology, "Edge"):
            try:
                parameter = Edge.ParameterAtVertex(topology, vertex)
            except:
                parameter = 400 #aribtrary large number greater than 1
            if parameter == None:
                return False
            return 0 <= parameter <= 1
        elif Topology.IsInstance(topology, "Wire"):
            vertices = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) > 1]
            edges = Wire.Edges(topology)
            sub_list = vertices + edges
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Face"):
            # Test the distance first
            if Vertex.PerpendicularDistance(vertex, topology) > tolerance:
                return False
            if Vertex.IsPeripheral(vertex, topology):
                return False
            normal = Face.Normal(topology)
            proj_v = Vertex.Project(vertex, topology)
            v1 = Topology.TranslateByDirectionDistance(proj_v, normal, 1)
            v2 = Topology.TranslateByDirectionDistance(proj_v, normal, -1)
            edge = Edge.ByVertices(v1, v2)
            intersect = edge.Intersect(topology)
            if intersect == None:
                return False
            return True
        elif Topology.IsInstance(topology, "Shell"):
            if Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent):
                return False
            else:
                edges = Topology.Edges(topology)
                for edge in edges:
                    if Vertex.IsInternal(vertex, edge, tolerance=tolerance, silent=silent):
                        return True
                faces = Topology.Faces(topology)
                for face in faces:
                    if Vertex.IsInternal(vertex, face, tolerance=tolerance, silent=silent):
                        return True
                return False
        elif Topology.IsInstance(topology, "Cell"):
            return topologic.CellUtility.Contains(topology, vertex, tolerance) == 0 # Hook to Core
        elif Topology.IsInstance(topology, "CellComplex"):
            ext_boundary = CellComplex.ExternalBoundary(topology)
            return Vertex.IsInternal(vertex, ext_boundary, tolerance=tolerance, silent=silent)
        elif Topology.IsInstance(topology, "Cluster"):
            sub_list = Cluster.FreeTopologies(topology)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        return False
    
    
    @staticmethod
    def IsPeripheral(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertex is peripheral to the input topology. Returns False otherwise.
        A vertex is said to be peripheral to the input topology if:
            01. Vertex: If it is internal to it (i.e. coincident with it).
            02. Edge: If it is internal to its start or end vertices.
            03. Manifold open wire: If it is internal to its start or end vertices.
            04. Manifold closed wire: If it is internal to any of its vertices.
            05. Non-manifold wire: If it is internal to any of its vertices that has a vertex degree of 1.
            06. Face: If it is internal to any of its edges or vertices.
            07. Shell: If it is internal to external boundary
            08. Cell: If it is internal to any of its faces, edges, or vertices.
            09. CellComplex: If it is peripheral to its external boundary.
            10. Cluster: If it is peripheral to any of its free topologies. (See Cluster.FreeTopologies)

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        tolerance : float , optional
            The tolerance for computing if the input vertex is peripheral to the input topology. The default is 0.0001.
        silent : bool , optional
            If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

        Returns
        -------
        bool
            True if the input vertex is peripheral to the input topology. False otherwise.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Wire import Wire
        from topologicpy.Face import Face
        from topologicpy.Shell import Shell
        from topologicpy.CellComplex import CellComplex
        from topologicpy.Cluster import Cluster
        from topologicpy.Topology import Topology
        
        if not Topology.IsInstance(vertex, "Vertex"):
            if not silent:
                print("Vertex.IsPeripheral - Error: The input vertex parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(topology, "Topology"):
            if not silent:
                print("Vertex.IsPeripheral - Error: The input topology parameter is not a valid topology. Returning None.")
            return None

        if Topology.IsInstance(topology, "Vertex"):
            return Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent)
        elif Topology.IsInstance(topology, "Edge"):
            sv = Edge.StartVertex(topology)
            ev = Edge.EndVertex(topology)
            f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
            f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
            return f1 or f2
        elif Topology.IsInstance(topology, "Wire"):
            if Wire.IsManifold(topology):
                if not Wire.IsClosed(topology):
                    sv = Wire.StartVertex(topology)
                    ev = Wire.EndVertex(topology)
                    f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
                    f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
                    return f1 or f2
                else:
                    sub_list = [v for v in Topology.Vertices(topology)]
                    for sub in sub_list:
                        if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                            return True
                    return False
            else:
                sub_list = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) == 1]
                for sub in sub_list:
                    if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                        return True
                return False
        elif Topology.IsInstance(topology, "Face"):
            sub_list = Topology.Vertices(topology) + Topology.Edges(topology)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Shell"):
            ext_boundary = Shell.ExternalBoundary(topology)
            sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Cell"):
            sub_list = Topology.Vertices(topology) + Topology.Edges(topology) + Topology.Faces(topology)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "CellComplex"):
            ext_boundary = CellComplex.ExternalBoundary(topology)
            sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary) + Topology.Faces(ext_boundary)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Cluster"):
            sub_list = Cluster.FreeTopologies(topology)
            for sub in sub_list:
                if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        return False
    
    @staticmethod
    def NearestVertex(vertex, topology, useKDTree: bool = True):
        """
        Returns the vertex found in the input topology that is the nearest to the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology to be searched for the nearest vertex.
        useKDTree : bool , optional
            if set to True, the algorithm will use a KDTree method to search for the nearest vertex. The default is True.

        Returns
        -------
        topologic_core.Vertex
            The nearest vertex.

        """
        from topologicpy.Topology import Topology

        def SED(a, b):
            """Compute the squared Euclidean distance between X and Y."""
            p1 = (a.X(), a.Y(), a.Z())
            p2 = (b.X(), b.Y(), b.Z())
            return sum((i-j)**2 for i, j in zip(p1, p2))
        
        BT = collections.namedtuple("BT", ["value", "left", "right"])
        BT.__doc__ = """
        A Binary Tree (BT) with a node value, and left- and
        right-subtrees.
        """
        def firstItem(v):
            return v.X()
        def secondItem(v):
            return v.Y()
        def thirdItem(v):
            return v.Z()

        def itemAtIndex(v, index):
            if index == 0:
                return v.X()
            elif index == 1:
                return v.Y()
            elif index == 2:
                return v.Z()

        def sortList(vertices, index):
            if index == 0:
                vertices.sort(key=firstItem)
            elif index == 1:
                vertices.sort(key=secondItem)
            elif index == 2:
                vertices.sort(key=thirdItem)
            return vertices
        
        def kdtree(topology):
            assert Topology.IsInstance(topology, "Topology"), "Vertex.NearestVertex: The input is not a Topology."
            vertices = []
            _ = topology.Vertices(None, vertices)
            assert (len(vertices) > 0), "Vertex.NearestVertex: Could not find any vertices in the input Topology"

            """Construct a k-d tree from an iterable of vertices.

            This algorithm is taken from Wikipedia. For more details,

            > https://en.wikipedia.org/wiki/K-d_tree#Construction

            """
            # k = len(points[0])
            k = 3

            def build(*, vertices, depth):
                if len(vertices) == 0:
                    return None
                #points.sort(key=operator.itemgetter(depth % k))
                vertices = sortList(vertices, (depth % k))

                middle = len(vertices) // 2
                
                return BT(
                    value = vertices[middle],
                    left = build(
                        vertices=vertices[:middle],
                        depth=depth+1,
                    ),
                    right = build(
                        vertices=vertices[middle+1:],
                        depth=depth+1,
                    ),
                )

            return build(vertices=list(vertices), depth=0)
        
        NNRecord = collections.namedtuple("NNRecord", ["vertex", "distance"])
        NNRecord.__doc__ = """
        Used to keep track of the current best guess during a nearest
        neighbor search.
        """

        def find_nearest_neighbor(*, tree, vertex):
            """Find the nearest neighbor in a k-d tree for a given vertex.
            """
            k = 3 # Forcing k to be 3 dimensional
            best = None
            def search(*, tree, depth):
                """Recursively search through the k-d tree to find the nearest neighbor.
                """
                nonlocal best

                if tree is None:
                    return
                distance = SED(tree.value, vertex)
                if best is None or distance < best.distance:
                    best = NNRecord(vertex=tree.value, distance=distance)

                axis = depth % k
                diff = itemAtIndex(vertex,axis) - itemAtIndex(tree.value,axis)
                if diff <= 0:
                    close, away = tree.left, tree.right
                else:
                    close, away = tree.right, tree.left

                search(tree=close, depth=depth+1)
                if diff**2 < best.distance:
                    search(tree=away, depth=depth+1)

            search(tree=tree, depth=0)
            return best.vertex
        
        if useKDTree:
            tree = kdtree(topology)
            return find_nearest_neighbor(tree=tree, vertex=vertex)
        else:
            vertices = []
            _ = topology.Vertices(None, vertices)
            distances = []
            indices = []
            for i in range(len(vertices)):
                distances.append(SED(vertex, vertices[i]))
                indices.append(i)
            sorted_indices = [x for _, x in sorted(zip(distances, indices))]
        return vertices[sorted_indices[0]]

    @staticmethod
    def Origin():
        """
        Returns a vertex with coordinates (0, 0, 0)

        Parameters
        -----------

        Return
        -----------
        topologic_core.Vertex
        """
        return Vertex.ByCoordinates(0, 0, 0)
    
    @staticmethod
    def PerpendicularDistance(vertex, face, mantissa: int = 6):
        """
        Returns the perpendicular distance between the input vertex and the input face. The face is considered to be infinite.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        face : topologic_core.Face
            The input face.
        mantissa: int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The distance between the input vertex and the input topology.

        """
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology
        import math

        def distance_point_to_line(point, line_start, line_end):
            # Convert input points to NumPy arrays for vector operations
            point = np.array(point)
            line_start = np.array(line_start)
            line_end = np.array(line_end)
            
            # Calculate the direction vector of the edge
            line_direction = line_end - line_start
            
            # Vector from the edge's starting point to the point
            point_to_start = point - line_start
            
            # Calculate the parameter 't' where the projection of the point onto the edge occurs
            if np.dot(line_direction, line_direction) == 0:
                t = 0
            else:
                t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
            
            # Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
            if t < 0:
                return np.linalg.norm(point - line_start)
            elif t > 1:
                return np.linalg.norm(point - line_end)
            
            # Calculate the closest point on the edge to the given point
            closest_point = line_start + t * line_direction
            
            # Calculate the distance between the closest point and the given point
            distance = np.linalg.norm(point - closest_point)
            
            return distance
        if not Topology.IsInstance(vertex, "Vertex"):
            print("Vertex.PerpendicularDistance - Error: The input vertex is not a valid topologic vertex. Returning None.")
            return None
        if not Topology.IsInstance(face, "Face"):
            print("Vertex.PerpendicularDistance - Error: The input face is not a valid topologic face. Returning None.")
            return None
        dic = Face.PlaneEquation(face)
        if dic == None: # The face is degenerate. Try to treat as an edge.
            point = Vertex.Coordinates(vertex)
            face_vertices = Topology.Vertices(face)
            line_start = Vertex.Coordinates(face_vertices[0])
            line_end = Vertex.Coordinates(face_vertices[1])
            return round(distance_point_to_line(point, line_start, line_end), mantissa)
        a = dic["a"]
        b = dic["b"]
        c = dic["c"]
        d = dic["d"]
        x1, y1, z1 = Vertex.Coordinates(vertex)
        d = abs((a * x1 + b * y1 + c * z1 + d))
        e = (math.sqrt(a * a + b * b + c * c))
        if e == 0:
            return 0
        return round(d/e, mantissa)
    
    @staticmethod
    def PlaneEquation(vertices, mantissa: int = 6):
        """
        Returns the equation of the average plane passing through a list of vertices.

        Parameters
        -----------
        vertices : list
            The input list of vertices
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Return
        -----------
        dict
            The dictionary containing the values of a, b, c, d for the plane equation in the form of ax+by+cz+d=0.
            The keys in the dictionary are ["a", "b", "c". "d"]
        """

        vertices = [Vertex.Coordinates(v) for v in vertices]
        # Convert vertices to a NumPy array for easier calculations
        vertices = np.array(vertices)

        # Calculate the centroid of the vertices
        centroid = np.mean(vertices, axis=0)

        # Center the vertices by subtracting the centroid
        centered_vertices = vertices - centroid

        # Calculate the covariance matrix
        covariance_matrix = np.dot(centered_vertices.T, centered_vertices)

        # Find the normal vector by computing the eigenvector of the smallest eigenvalue
        _, eigen_vectors = np.linalg.eigh(covariance_matrix)
        normal_vector = eigen_vectors[:, 0]

        # Normalize the normal vector
        normal_vector /= np.linalg.norm(normal_vector)

        # Calculate the constant D using the centroid and the normal vector
        d = -np.dot(normal_vector, centroid)
        d = round(d, mantissa)

        # Create the plane equation in the form Ax + By + Cz + D = 0
        a, b, c = normal_vector
        a = round(a, mantissa)
        b = round(b, mantissa)
        c = round(c, mantissa)

        return {"a":a, "b":b, "c":c, "d":d}
    
    @staticmethod
    def Point(x=0, y=0, z=0):
        """
        Creates a point (vertex) using the input parameters

        Parameters
        -----------
        x : float , optional.
            The desired x coordinate. The default is 0.
        y : float , optional.
            The desired y coordinate. The default is 0.
        z : float , optional.
            The desired z coordinate. The default is 0.

        Return
        -----------
        topologic_core.Vertex
        """
        
        return Vertex.ByCoordinates(x, y, z)

    @staticmethod
    def Project(vertex, face, direction: bool = None, mantissa: int = 6):
        """
        Returns a vertex that is the projection of the input vertex unto the input face.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex to project unto the input face.
        face : topologic_core.Face
            The input face that receives the projection of the input vertex.
        direction : vector, optional
            The direction in which to project the input vertex unto the input face. If not specified, the direction of the projection is the normal of the input face. The default is None.
        mantissa : int , optional
            The length of the desired mantissa. The default is 6.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        topologic_core.Vertex
            The projected vertex.

        """
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology
        
        def project_point_onto_plane(point, plane_coeffs, direction_vector):
            """
            Project a 3D point onto a plane defined by its coefficients and using a direction vector.

            Parameters:
                point (tuple or list): The 3D point coordinates (x, y, z).
                plane_coeffs (tuple or list): The coefficients of the plane equation (a, b, c, d).
                direction_vector (tuple or list): The direction vector (vx, vy, vz).

            Returns:
                tuple: The projected point coordinates (x_proj, y_proj, z_proj).
            """
            # Unpack point coordinates
            x, y, z = point

            # Unpack plane coefficients
            a, b, c, d = plane_coeffs

            # Unpack direction vector
            vx, vy, vz = direction_vector

            # Calculate the distance from the point to the plane
            distance = (a * x + b * y + c * z + d) / (a * vx + b * vy + c * vz)

            # Calculate the projected point coordinates
            x_proj = x - distance * vx
            y_proj = y - distance * vy
            z_proj = z - distance * vz

            return [x_proj, y_proj, z_proj]

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        if not Topology.IsInstance(face, "Face"):
            return None
        eq = Face.PlaneEquation(face, mantissa= mantissa)
        if direction == None or direction == []:
            direction = Face.Normal(face)
        pt = project_point_onto_plane(Vertex.Coordinates(vertex), [eq["a"], eq["b"], eq["c"], eq["d"]], direction)
        return Vertex.ByCoordinates(pt[0], pt[1], pt[2])

    @staticmethod
    def X(vertex, mantissa: int = 6) -> float:
        """
        Returns the X coordinate of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The X coordinate of the input vertex.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        return round(vertex.X(), mantissa)

    @staticmethod
    def Y(vertex, mantissa: int = 6) -> float:
        """
        Returns the Y coordinate of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The Y coordinate of the input vertex.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        return round(vertex.Y(), mantissa)

    @staticmethod
    def Z(vertex, mantissa: int = 6) -> float:
        """
        Returns the Z coordinate of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The Z coordinate of the input vertex.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        return round(vertex.Z(), mantissa)
           

Classes

class Vertex
Expand source code
class Vertex():
    @staticmethod
    def AreCollinear(vertices: list, tolerance: float = 0.0001):
        """
        Returns True if the input list of vertices form a straight line. Returns False otherwise.

        Parameters
        ----------
        vertices : list
            The input list of vertices.
        tolerance : float, optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise.

        """
        from topologicpy.Cluster import Cluster
        from topologicpy.Topology import Topology
        from topologicpy.Vector import Vector
        import sys
        def areCollinear(vertices, tolerance=0.0001):
            point1 = [Vertex.X(vertices[0]), Vertex.Y(vertices[0]), Vertex.Z(vertices[0])]
            point2 = [Vertex.X(vertices[1]), Vertex.Y(vertices[1]), Vertex.Z(vertices[1])]
            point3 = [Vertex.X(vertices[2]), Vertex.Y(vertices[2]), Vertex.Z(vertices[2])]

            vector1 = [point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]]
            vector2 = [point3[0] - point1[0], point3[1] - point1[1], point3[2] - point1[2]]

            cross_product_result = Vector.Cross(vector1, vector2, tolerance=tolerance)
            return cross_product_result == None
        
        if not isinstance(vertices, list):
            print("Vertex.AreCollinear - Error: The input list of vertices is not a valid list. Returning None.")
            return None
        vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
        if len(vertexList) < 2:
            print("Vertex.AreCollinear - Error: The input list of vertices does not contain sufficient valid vertices. Returning None.")
            return None
        if len(vertexList) < 3:
            return True # Any two vertices can form a line!
        cluster = Topology.SelfMerge(Cluster.ByTopologies(vertexList), tolerance=tolerance)
        vertexList = Topology.Vertices(cluster)
        slices = []
        for i in range(2,len(vertexList)):
            slices.append([vertexList[0], vertexList[1], vertexList[i]])
        for slice in slices:
            if not areCollinear(slice, tolerance=tolerance):
                return False
        return True
    
    @staticmethod
    def AreIpsilateral(vertices: list, face) -> bool:
        """
        Returns True if the input list of vertices are on one side of a face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        vertices : list
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        def check(dot_productA, pointB, pointC, normal):
            # Calculate the dot products of the vectors from the surface point to each of the input points.
            dot_productB = (pointB[0] - pointC[0]) * normal[0] + \
                        (pointB[1] - pointC[1]) * normal[1] + \
                        (pointB[2] - pointC[2]) * normal[2]

            # Check if both points are on the same side of the surface.
            if dot_productA * dot_productB > 0:
                return True

            # Check if both points are on opposite sides of the surface.
            elif dot_productA * dot_productB < 0:
                return False

            # Otherwise, at least one point is on the surface.
            else:
                return True
    
        from topologicpy.Vertex import Vertex
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(face, "Face"):
            return None
        vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
        if len(vertexList) < 2:
            return None
        pointA = Vertex.Coordinates(vertexList[0])
        pointC = Vertex.Coordinates(Face.VertexByParameters(face, 0.5, 0.5))
        normal = Face.Normal(face)
        dot_productA = (pointA[0] - pointC[0]) * normal[0] + \
                        (pointA[1] - pointC[1]) * normal[1] + \
                        (pointA[2] - pointC[2]) * normal[2]
        for i in range(1, len(vertexList)):
            pointB = Vertex.Coordinates(vertexList[i])
            if not check(dot_productA, pointB, pointC, normal):
                return False
        return True
    
    @staticmethod
    def AreIpsilateralCluster(cluster, face) -> bool:
        """
        Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(cluster, "Topology"):
            return None
        vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
        return Vertex.AreIpsilateral(vertices, face)
    
    @staticmethod
    def AreOnSameSide(vertices: list, face) -> bool:
        """
        Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        vertices : list
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        return Vertex.AreIpsilateral(vertices, face)

    @staticmethod
    def AreOnSameSideCluster(cluster, face) -> bool:
        """
        Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input list of vertices.
        face : topologic_core.Face
            The input face

        Returns
        -------
        bool
            True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(cluster, "Topology"):
            return None
        vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
        return Vertex.AreIpsilateral(vertices, face)

    @staticmethod
    def ByCoordinates(*args, **kwargs):
        """
        Creates a vertex at the coordinates specified by the x, y, z inputs. You can call this method using a list of coordinates or individually.
        Examples:
        v = Vertex.ByCoordinates(3.4, 5.7, 2.8)
        v = Vertex.ByCoordinates([3.4, 5.7, 2.8])
        v = Vertex.ByCoordinates(x=3.4, y=5.7, z=2.8)

        Parameters
        ----------
        x : float , optional
            The X coordinate. The default is 0.
        y : float , optional
            The Y coordinate. The default is 0.
        z : float , optional
            The Z coordinate. The defaults is 0.

        Returns
        -------
        topologic_core.Vertex
            The created vertex.

        """
        import numbers
        x = None
        y = None
        z = None
        if len(args) > 3 or len(kwargs.items()) > 3:
            print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
            return None
        if len(args) > 0:
            value = args[0]
            if isinstance(value, list) and len(value) > 3:
                print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
                return None
            elif isinstance(value, list) and len(value) == 3:
                x = value[0]
                y = value[1]
                z = value[2]
            elif isinstance(value, list) and len(value) == 2:
                x = value[0]
                y = value[1]
            elif isinstance(value, list) and len(value) == 1:
                x = value[0]
            elif len(args) == 3:
                x = args[0]
                y = args[1]
                z = args[2]
            elif len(args) == 2:
                x = args[0]
                y = args[1]
            elif len(args) == 1:
                x = args[0]
        for key, value in kwargs.items():
            if "x" in key.lower():
                if not x == None:
                    print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                    return None
                x = value
            elif "y" in key.lower():
                if not y == None:
                    print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                    return None
                y = value
            elif "z" in key.lower():
                if not z == None:
                    print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                    return None
                z = value
        if x == None:
            x = 0
        if y == None:
            y = 0
        if z == None:
            z = 0
        if not isinstance(x, numbers.Number):
            print("Vertex.ByCoordinates - Error: The x value is not a valid number. Returning None.")
            return None
        if not isinstance(y, numbers.Number):
            print("Vertex.ByCoordinates - Error: The y value is not a valid number. Returning None.")
            return None
        if not isinstance(z, numbers.Number):
            print("Vertex.ByCoordinates - Error: The z value is not a valid number. Returning None.")
            return None
        
        vertex = None
        try:
            vertex = topologic.Vertex.ByCoordinates(x, y, z) # Hook to Core
        except:
            vertex = None
            print("Vertex.ByCoordinates - Error: Could not create a topologic vertex. Returning None.")
        return vertex
    
    @staticmethod
    def Centroid(vertices):
        """
        Returns the centroid of the input list of vertices.

        Parameters
        -----------
        vertices : list
            The input list of vertices

        Return
        ----------
        topologic_core.Vertex
            The computed centroid of the input list of vertices
        
        """
        from topologicpy.Topology import Topology

        if not isinstance(vertices, list):
            print("Vertex.Centroid - Error: The input vertices parameter is not a valid list. Returning None.")
            return None
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) < 1:
            print("Vertex.Centroid - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
            return None
        if len(vertices) == 1:
            return vertices[0]
        cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
        cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)
        cz = sum(Vertex.Z(v) for v in vertices) / len(vertices)
        return Vertex.ByCoordinates(cx, cy, cz)
    
    @staticmethod
    def Clockwise2D(vertices):
        """
        Sorts the input list of vertices in a clockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

        Parameters
        -----------
        vertices : list
            The input list of vertices

        Return
        -----------
        list
            The input list of vertices sorted in a counter clockwise fashion
        
        """
        return list(reversed(Vertex.CounterClockwise2D(vertices)))
    
    @staticmethod
    def Coordinates(vertex, outputType: str = "xyz", mantissa: int = 6) -> list:
        """
        Returns the coordinates of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        outputType : string, optional
            The desired output type. Could be any permutation or substring of "xyz" or the string "matrix". The default is "xyz". The input is case insensitive and the coordinates will be returned in the specified order.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        list
            The coordinates of the input vertex.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        x = round(vertex.X(), mantissa)
        y = round(vertex.Y(), mantissa)
        z = round(vertex.Z(), mantissa)
        matrix = [[1, 0, 0, x],
                [0, 1, 0, y],
                [0, 0, 1, z],
                [0, 0, 0, 1]]
        output = []
        outputType = outputType.lower()
        if outputType == "matrix":
            return matrix
        else:
            outputType = list(outputType)
            for axis in outputType:
                if axis == "x":
                    output.append(x)
                elif axis == "y":
                    output.append(y)
                elif axis == "z":
                    output.append(z)
        return output

    @staticmethod
    def CounterClockwise2D(vertices):
        """
        Sorts the input list of vertices in a counterclockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

        Parameters
        -----------
        vertices : list
            The input list of vertices

        Return
        -----------
        list
            The input list of vertices sorted in a counter clockwise fashion
        
        """
        import math
        # find the centroid of the points
        cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
        cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)

        # sort the points based on their angle with respect to the centroid
        vertices.sort(key=lambda v: (math.atan2(Vertex.Y(v) - cy, Vertex.X(v) - cx) + 2 * math.pi) % (2 * math.pi))
        return vertices

    @staticmethod
    def Degree(vertex, hostTopology, topologyType: str = "edge"):
        """
        Returns the vertex degree (the number of super topologies connected to it). See https://en.wikipedia.org/wiki/Degree_(graph_theory).

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        hostTopology : topologic_core.Topology
            The input host topology in which to search for the connected super topologies.
        topologyType : str , optional
            The topology type to search for. This can be any of "edge", "wire", "face", "shell", "cell", "cellcomplex", "cluster". It is case insensitive. If set to None, the immediate supertopology type is searched for. The default is None.

        Returns
        -------
        int
            The number of super topologies connected to this vertex

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            print("Vertex.Degree - Error: The input vertex parameter is not a valid topologic vertex. Returning None.")
        if not Topology.IsInstance(hostTopology, "Topology"):
            print("Vertex.Degree - Error: The input hostTopology parameter is not a valid topologic topology. Returning None.")
        superTopologies = Topology.SuperTopologies(topology=vertex, hostTopology=hostTopology, topologyType=topologyType)
        return len(superTopologies)


    @staticmethod
    def Distance(vertex, topology, includeCentroid: bool =True,
                 mantissa: int = 6) -> float:
        """
        Returns the distance between the input vertex and the input topology. This method returns the distance to the closest sub-topology in the input topology, optionally including its centroid.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        includeCentroid : bool
            If set to True, the centroid of the input topology will be considered in finding the nearest subTopology to the input vertex. The default is True.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.
        
        Returns
        -------
        float
            The distance between the input vertex and the input topology.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology
        import math

        def distance_point_to_point(point1, point2):
            # Convert input points to NumPy arrays
            point1 = np.array(point1)
            point2 = np.array(point2)
            
            # Calculate the Euclidean distance
            distance = np.linalg.norm(point1 - point2)
            
            return distance

        def distance_point_to_line(point, line_start, line_end):
            # Convert input points to NumPy arrays for vector operations
            point = np.array(point)
            line_start = np.array(line_start)
            line_end = np.array(line_end)
            
            # Calculate the direction vector of the edge
            line_direction = line_end - line_start
            
            # Vector from the edge's starting point to the point
            point_to_start = point - line_start
            
            # Calculate the parameter 't' where the projection of the point onto the edge occurs
            if np.dot(line_direction, line_direction) == 0:
                t = 0
            else:
                t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
            
            # Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
            if t < 0:
                return np.linalg.norm(point - line_start)
            elif t > 1:
                return np.linalg.norm(point - line_end)
            
            # Calculate the closest point on the edge to the given point
            closest_point = line_start + t * line_direction
            
            # Calculate the distance between the closest point and the given point
            distance = np.linalg.norm(point - closest_point)
            
            return distance
        
        def distance_to_vertex(vertexA, vertexB):
            a = (Vertex.X(vertexA), Vertex.Y(vertexA), Vertex.Z(vertexA))
            b = (Vertex.X(vertexB), Vertex.Y(vertexB), Vertex.Z(vertexB))
            return distance_point_to_point(a, b)
        
        def distance_to_edge(vertex, edge):
            a = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
            sv = Edge.StartVertex(edge)
            ev = Edge.EndVertex(edge)
            svp = (Vertex.X(sv), Vertex.Y(sv), Vertex.Z(sv))
            evp = (Vertex.X(ev), Vertex.Y(ev), Vertex.Z(ev))
            return distance_point_to_line(a,svp, evp)
        
        def distance_to_face(vertex, face, includeCentroid):
            v_proj = Vertex.Project(vertex, face, mantissa=mantissa)
            if not Vertex.IsInternal(v_proj, face):
                vertices = Topology.Vertices(topology)
                distances = [distance_to_vertex(vertex, v) for v in vertices]
                edges = Topology.Edges(topology)
                distances += [distance_to_edge(vertex, e) for e in edges]
                if includeCentroid:
                    distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
                return min(distances)
            dic = Face.PlaneEquation(face)
            a = dic["a"]
            b = dic["b"]
            c = dic["c"]
            d = dic["d"]
            x1, y1, z1 = Vertex.Coordinates(vertex)
            d = abs((a * x1 + b * y1 + c * z1 + d))
            e = (math.sqrt(a * a + b * b + c * c))
            if e == 0:
                return 0
            return d/e
        if not Topology.IsInstance(vertex, "Vertex") or not Topology.IsInstance(topology, "Topology"):
            return None
        if Topology.IsInstance(topology, "Vertex"):
            return round(distance_to_vertex(vertex,topology), mantissa)
        elif Topology.IsInstance(topology, "Edge"):
            return round(distance_to_edge(vertex,topology), mantissa)
        elif Topology.IsInstance(topology, "Wire"):
            vertices = Topology.Vertices(topology)
            distances = [distance_to_vertex(vertex, v) for v in vertices]
            edges = Topology.Edges(topology)
            distances += [distance_to_edge(vertex, e) for e in edges]
            if includeCentroid:
                distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
            return round(min(distances), mantissa)
        elif Topology.IsInstance(topology, "Face"):
            vertices = Topology.Vertices(topology)
            distances = [distance_to_vertex(vertex, v) for v in vertices]
            edges = Topology.Edges(topology)
            distances += [distance_to_edge(vertex, e) for e in edges]
            distances.append(distance_to_face(vertex,topology, includeCentroid))
            if includeCentroid:
                distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
            return round(min(distances), mantissa)
        elif Topology.IsInstance(topology, "Shell") or Topology.IsInstance(topology, "Cell") or Topology.IsInstance(topology, "CellComplex") or Topology.IsInstance(topology, "Cluster"):
            vertices = Topology.Vertices(topology)
            distances = [distance_to_vertex(vertex, v) for v in vertices]
            edges = Topology.Edges(topology)
            distances += [distance_to_edge(vertex, e) for e in edges]
            faces = Topology.Faces(topology)
            distances += [distance_to_face(vertex, f, includeCentroid) for f in faces]
            if includeCentroid:
                distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
            return round(min(distances), mantissa)
        else:
            print("Vertex.Distance - Error: Could not recognize the input topology. Returning None.")
            return None
    
    @staticmethod
    def EnclosingCell(vertex, topology, exclusive: bool = True, tolerance: float = 0.0001) -> list:
        """
        Returns the list of Cells found in the input topology that enclose the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        exclusive : bool , optional
            If set to True, return only the first found enclosing cell. The default is True.
        tolerance : float , optional
            The tolerance for computing if the input vertex is enclosed in a cell. The default is 0.0001.

        Returns
        -------
        list
            The list of enclosing cells.

        """
        from topologicpy.Cell import Cell
        from topologicpy.Topology import Topology
        
        def boundingBox(cell):
            vertices = []
            _ = cell.Vertices(None, vertices)
            x = []
            y = []
            z = []
            for aVertex in vertices:
                x.append(aVertex.X())
                y.append(aVertex.Y())
                z.append(aVertex.Z())
            return ([min(x), min(y), min(z), max(x), max(y), max(z)])
        
        if Topology.IsInstance(topology, "Cell"):
            cells = [topology]
        elif Topology.IsInstance(topology, "Cluster") or Topology.IsInstance(topology, "CellComplex"):
            cells = []
            _ = topology.Cells(None, cells)
        else:
            return None
        if len(cells) < 1:
            return None
        enclosingCells = []
        for i in range(len(cells)):
            bbox = boundingBox(cells[i])
            if ((vertex.X() < bbox[0]) or (vertex.Y() < bbox[1]) or (vertex.Z() < bbox[2]) or (vertex.X() > bbox[3]) or (vertex.Y() > bbox[4]) or (vertex.Z() > bbox[5])) == False:
                if Vertex.IsInternal(vertex, cells[i], tolerance=tolerance):
                    if exclusive:
                        return([cells[i]])
                    else:
                        enclosingCells.append(cells[i])
        return enclosingCells

    @staticmethod
    def Fuse(vertices: list, mantissa: int = 6, tolerance: float = 0.0001):
        """
        Returns a list of vertices where vertices within a specified tolerance distance are fused while retaining duplicates, ensuring that vertices with nearly identical coordinates are replaced by a single shared coordinate.

        Parameters
        ----------
        vertices : list
            The input list of topologic vertices.
        mantissa : int , optional
            The desired length of the mantissa for retrieving vertex coordinates. The default is 6.
        tolerance : float , optional
            The desired tolerance for computing if vertices need to be fused. Any vertices that are closer to each other than this tolerance will be fused. The default is 0.0001.

         Returns
        -------
        list
            The list of fused vertices. This list contains the same number of vertices and in the same order as the input list of vertices. However, the coordinates
            of these vertices have now been modified so that they are exactly the same with other vertices that are within the tolerance distance.
        """
        from topologicpy.Topology import Topology
        import numpy as np

        def fuse_vertices(vertices, tolerance):
            fused_vertices = []
            merged_indices = {}

            for idx, vertex in enumerate(vertices):
                if idx in merged_indices:
                    fused_vertices.append(fused_vertices[merged_indices[idx]])
                    continue

                merged_indices[idx] = len(fused_vertices)
                fused_vertex = vertex
                for i in range(idx + 1, len(vertices)):
                    if i in merged_indices:
                        continue

                    other_vertex = vertices[i]
                    distance = np.linalg.norm(np.array(vertex) - np.array(other_vertex))
                    if distance < tolerance:
                        # Choose the coordinate with the least amount of decimal points
                        if count_decimal_points(other_vertex) < count_decimal_points(fused_vertex):
                            fused_vertex = other_vertex

                        merged_indices[i] = len(fused_vertices)

                fused_vertices.append(fused_vertex)

            return fused_vertices
        def count_decimal_points(vertex):
            # Count the number of decimal points in the coordinates
            decimals_list = []
            for coord in vertex:
                coord_str = str(coord)
                if '.' in coord_str:
                    decimals_list.append(len(coord_str.split('.')[1]))
                elif 'e' in coord_str:
                    decimals_list.append(int(coord_str.split('e')[1].replace('-','')))
            return max(decimals_list)


        if not isinstance(vertices, list):
            print("Vertex.Fuse - Error: The input vertices parameter is not a valid list. Returning None.")
            return None
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) == 0:
            print("Vertex.Fuse - Error: The input vertices parameter does not contain any valid topologic vertices. Returning None.")
            return None
        
        vertices = [(Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa), Vertex.Z(v, mantissa=mantissa)) for v in vertices]
        fused_vertices = fuse_vertices(vertices, tolerance)
        return_vertices = [Vertex.ByCoordinates(list(coord)) for coord in fused_vertices]
        return return_vertices

    @staticmethod
    def Index(vertex, vertices: list, strict: bool = False, tolerance: float = 0.0001) -> int:
        """
        Returns index of the input vertex in the input list of vertices

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        vertices : list
            The input list of vertices.
        strict : bool , optional
            If set to True, the vertex must be strictly identical to the one found in the list. Otherwise, a distance comparison is used. The default is False.
        tolerance : float , optional
            The tolerance for computing if the input vertex is identical to a vertex from the list. The default is 0.0001.

        Returns
        -------
        int
            The index of the input vertex in the input list of vertices.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        if not isinstance(vertices, list):
            return None
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) == 0:
            return None
        for i in range(len(vertices)):
            if strict:
                if Topology.IsSame(vertex, vertices[i]):
                    return i
            else:
                d = Vertex.Distance(vertex, vertices[i])
                if d < tolerance:
                    return i
        return None

    @staticmethod
    def InterpolateValue(vertex, vertices, n=3, key="intensity", tolerance=0.0001):
        """
        Interpolates the value of the input vertex based on the values of the *n* nearest vertices.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        vertices : list
            The input list of vertices.
        n : int , optional
            The maximum number of nearest vertices to consider. The default is 3.
        key : str , optional
            The key that holds the value to be interpolated in the dictionaries of the vertices. The default is "intensity".
        tolerance : float , optional
            The tolerance for computing if the input vertex is coincident with another vertex in the input list of vertices. The default is 0.0001.

        Returns
        -------
        topologic_core.vertex
            The input vertex with the interpolated value stored in its dictionary at the key specified by the input key. Other keys and values in the dictionary are preserved.

        """

        def interpolate_value(point, data_points, n, tolerance=0.0001):
            """
            Interpolates the value associated with a point in 3D by averaging the values of the n nearest points.
            The influence of the adjacent points is inversely proportional to their distance from the input point.

            Args:
                data_points (list): A list of tuples, each representing a data point in 3D space as (x, y, z, value).
                                    The 'value' represents the value associated with that data point.
                point (tuple): A tuple representing the point in 3D space as (x, y, z) for which we want to interpolate a value.
                n (int): The number of nearest points to consider for interpolation.

            Returns:
                The interpolated value for the input point.
            """
            # Calculate the distances between the input point and all data points
            distances = [(distance(p[:3], point), p[3]) for p in data_points]

            # Sort the distances in ascending order
            sorted_distances = sorted(distances, key=lambda x: x[0])

            # Take the n nearest points
            nearest_points = sorted_distances[:n]

            n_p = nearest_points[0]
            n_d = n_p[0]
            if n_d < tolerance:
                return n_p[1]

            # Calculate the weights for each nearest point based on inverse distance

            weights = [(1/d[0], d[1]) for d in nearest_points]

            # Normalize the weights so they sum to 1
            total_weight = sum(w[0] for w in weights)
            normalized_weights = [(w[0]/total_weight, w[1]) for w in weights]

            # Interpolate the value as the weighted average of the nearest points
            interpolated_value = sum(w[0]*w[1] for w in normalized_weights)

            return interpolated_value

        def distance(point1, point2):
            """
            Calculates the Euclidean distance between two points in 3D space.

            Args:
                point1 (tuple): A tuple representing a point in 3D space as (x, y, z).
                point2 (tuple): A tuple representing a point in 3D space as (x, y, z).

            Returns:
                The Euclidean distance between the two points.
            """
            return ((point1[0]-point2[0])**2 + (point1[1]-point2[1])**2 + (point1[2]-point2[2])**2)**0.5
        
        from topologicpy.Topology import Topology
        from topologicpy.Dictionary import Dictionary

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        if not isinstance(vertices, list):
            return None
        
        vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
        if len(vertices) == 0:
            return None
        
        point = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
        data_points = []
        for v in vertices:
            d = Topology.Dictionary(v)
            value = Dictionary.ValueAtKey(d, key)
            if not value == None:
                if type(value) == int or type(value) == float:
                    data_points.append((Vertex.X(v), Vertex.Y(v), Vertex.Z(v), value))
        if len(data_points) == 0:
            return None
        if n > len(data_points):
            n = len(data_points)
        value = interpolate_value(point, data_points, n, tolerance=0.0001)
        d = Topology.Dictionary(vertex)
        d = Dictionary.SetValueAtKey(d, key, value)
        vertex = Topology.SetDictionary(vertex, d)
        return vertex

    @staticmethod
    def IsCoincident(vertexA, vertexB, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertexA is coincident with the input vertexB. Returns False otherwise.

        Parameters
        ----------
        vertexA : topologic_core.Vertex
            The first input vertex.
        vertexB : topologic_core.Vertex
            The second input vertex.
        tolerance : float , optional
            The tolerance for computing if the input vertexA is coincident with the input vertexB. The default is 0.0001.

        Returns
        -------
        bool
            True if the input vertexA is coincident with the input vertexB. False otherwise.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertexA, "Vertex"):
            if not silent:
                print("Vertex.IsCoincident - Error: The input vertexA parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(vertexB, "Vertex"):
            if not silent:
                print("Vertex.IsICoincident - Error: The input vertexB parameter is not a valid vertex. Returning None.")
            return None
        return Vertex.IsInternal(vertexA, vertexB, tolerance=tolerance, silent=silent)

    @staticmethod
    def IsExternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertex is external to the input topology. Returns False otherwise.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        tolerance : float , optional
            The tolerance for computing if the input vertex is external to the input topology. The default is 0.0001.
        silent : bool , optional
            If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

        Returns
        -------
        bool
            True if the input vertex is external to the input topology. False otherwise.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            if not silent:
                print("Vertex.IsExternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(topology, "Topology"):
            if not silent:
                print("Vertex.IsExternal - Error: The input topology parameter is not a valid topology. Returning None.")
            return None
        return not (Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent) or Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent))
    
    @staticmethod
    def IsInternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertex is inside the input topology. Returns False otherwise.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        tolerance : float , optional
            The tolerance for computing if the input vertex is internal to the input topology. The default is 0.0001.
        silent : bool , optional
            If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

        Returns
        -------
        bool
            True if the input vertex is internal to the input topology. False otherwise.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Wire import Wire
        from topologicpy.Face import Face
        from topologicpy.CellComplex import CellComplex
        from topologicpy.Cluster import Cluster
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            if not silent:
                print("Vertex.IsInternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(topology, "Topology"):
            if not silent:
                print("Vertex.IsInternal - Error: The input topology parameter is not a valid topology. Returning None.")
            return None

        if Topology.IsInstance(topology, "Vertex"):
            return Vertex.Distance(vertex, topology) < tolerance
        elif Topology.IsInstance(topology, "Edge"):
            try:
                parameter = Edge.ParameterAtVertex(topology, vertex)
            except:
                parameter = 400 #aribtrary large number greater than 1
            if parameter == None:
                return False
            return 0 <= parameter <= 1
        elif Topology.IsInstance(topology, "Wire"):
            vertices = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) > 1]
            edges = Wire.Edges(topology)
            sub_list = vertices + edges
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Face"):
            # Test the distance first
            if Vertex.PerpendicularDistance(vertex, topology) > tolerance:
                return False
            if Vertex.IsPeripheral(vertex, topology):
                return False
            normal = Face.Normal(topology)
            proj_v = Vertex.Project(vertex, topology)
            v1 = Topology.TranslateByDirectionDistance(proj_v, normal, 1)
            v2 = Topology.TranslateByDirectionDistance(proj_v, normal, -1)
            edge = Edge.ByVertices(v1, v2)
            intersect = edge.Intersect(topology)
            if intersect == None:
                return False
            return True
        elif Topology.IsInstance(topology, "Shell"):
            if Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent):
                return False
            else:
                edges = Topology.Edges(topology)
                for edge in edges:
                    if Vertex.IsInternal(vertex, edge, tolerance=tolerance, silent=silent):
                        return True
                faces = Topology.Faces(topology)
                for face in faces:
                    if Vertex.IsInternal(vertex, face, tolerance=tolerance, silent=silent):
                        return True
                return False
        elif Topology.IsInstance(topology, "Cell"):
            return topologic.CellUtility.Contains(topology, vertex, tolerance) == 0 # Hook to Core
        elif Topology.IsInstance(topology, "CellComplex"):
            ext_boundary = CellComplex.ExternalBoundary(topology)
            return Vertex.IsInternal(vertex, ext_boundary, tolerance=tolerance, silent=silent)
        elif Topology.IsInstance(topology, "Cluster"):
            sub_list = Cluster.FreeTopologies(topology)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        return False
    
    
    @staticmethod
    def IsPeripheral(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
        """
        Returns True if the input vertex is peripheral to the input topology. Returns False otherwise.
        A vertex is said to be peripheral to the input topology if:
            01. Vertex: If it is internal to it (i.e. coincident with it).
            02. Edge: If it is internal to its start or end vertices.
            03. Manifold open wire: If it is internal to its start or end vertices.
            04. Manifold closed wire: If it is internal to any of its vertices.
            05. Non-manifold wire: If it is internal to any of its vertices that has a vertex degree of 1.
            06. Face: If it is internal to any of its edges or vertices.
            07. Shell: If it is internal to external boundary
            08. Cell: If it is internal to any of its faces, edges, or vertices.
            09. CellComplex: If it is peripheral to its external boundary.
            10. Cluster: If it is peripheral to any of its free topologies. (See Cluster.FreeTopologies)

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology.
        tolerance : float , optional
            The tolerance for computing if the input vertex is peripheral to the input topology. The default is 0.0001.
        silent : bool , optional
            If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

        Returns
        -------
        bool
            True if the input vertex is peripheral to the input topology. False otherwise.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Wire import Wire
        from topologicpy.Face import Face
        from topologicpy.Shell import Shell
        from topologicpy.CellComplex import CellComplex
        from topologicpy.Cluster import Cluster
        from topologicpy.Topology import Topology
        
        if not Topology.IsInstance(vertex, "Vertex"):
            if not silent:
                print("Vertex.IsPeripheral - Error: The input vertex parameter is not a valid vertex. Returning None.")
            return None
        if not Topology.IsInstance(topology, "Topology"):
            if not silent:
                print("Vertex.IsPeripheral - Error: The input topology parameter is not a valid topology. Returning None.")
            return None

        if Topology.IsInstance(topology, "Vertex"):
            return Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent)
        elif Topology.IsInstance(topology, "Edge"):
            sv = Edge.StartVertex(topology)
            ev = Edge.EndVertex(topology)
            f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
            f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
            return f1 or f2
        elif Topology.IsInstance(topology, "Wire"):
            if Wire.IsManifold(topology):
                if not Wire.IsClosed(topology):
                    sv = Wire.StartVertex(topology)
                    ev = Wire.EndVertex(topology)
                    f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
                    f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
                    return f1 or f2
                else:
                    sub_list = [v for v in Topology.Vertices(topology)]
                    for sub in sub_list:
                        if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                            return True
                    return False
            else:
                sub_list = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) == 1]
                for sub in sub_list:
                    if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                        return True
                return False
        elif Topology.IsInstance(topology, "Face"):
            sub_list = Topology.Vertices(topology) + Topology.Edges(topology)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Shell"):
            ext_boundary = Shell.ExternalBoundary(topology)
            sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Cell"):
            sub_list = Topology.Vertices(topology) + Topology.Edges(topology) + Topology.Faces(topology)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "CellComplex"):
            ext_boundary = CellComplex.ExternalBoundary(topology)
            sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary) + Topology.Faces(ext_boundary)
            for sub in sub_list:
                if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        elif Topology.IsInstance(topology, "Cluster"):
            sub_list = Cluster.FreeTopologies(topology)
            for sub in sub_list:
                if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
        return False
    
    @staticmethod
    def NearestVertex(vertex, topology, useKDTree: bool = True):
        """
        Returns the vertex found in the input topology that is the nearest to the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        topology : topologic_core.Topology
            The input topology to be searched for the nearest vertex.
        useKDTree : bool , optional
            if set to True, the algorithm will use a KDTree method to search for the nearest vertex. The default is True.

        Returns
        -------
        topologic_core.Vertex
            The nearest vertex.

        """
        from topologicpy.Topology import Topology

        def SED(a, b):
            """Compute the squared Euclidean distance between X and Y."""
            p1 = (a.X(), a.Y(), a.Z())
            p2 = (b.X(), b.Y(), b.Z())
            return sum((i-j)**2 for i, j in zip(p1, p2))
        
        BT = collections.namedtuple("BT", ["value", "left", "right"])
        BT.__doc__ = """
        A Binary Tree (BT) with a node value, and left- and
        right-subtrees.
        """
        def firstItem(v):
            return v.X()
        def secondItem(v):
            return v.Y()
        def thirdItem(v):
            return v.Z()

        def itemAtIndex(v, index):
            if index == 0:
                return v.X()
            elif index == 1:
                return v.Y()
            elif index == 2:
                return v.Z()

        def sortList(vertices, index):
            if index == 0:
                vertices.sort(key=firstItem)
            elif index == 1:
                vertices.sort(key=secondItem)
            elif index == 2:
                vertices.sort(key=thirdItem)
            return vertices
        
        def kdtree(topology):
            assert Topology.IsInstance(topology, "Topology"), "Vertex.NearestVertex: The input is not a Topology."
            vertices = []
            _ = topology.Vertices(None, vertices)
            assert (len(vertices) > 0), "Vertex.NearestVertex: Could not find any vertices in the input Topology"

            """Construct a k-d tree from an iterable of vertices.

            This algorithm is taken from Wikipedia. For more details,

            > https://en.wikipedia.org/wiki/K-d_tree#Construction

            """
            # k = len(points[0])
            k = 3

            def build(*, vertices, depth):
                if len(vertices) == 0:
                    return None
                #points.sort(key=operator.itemgetter(depth % k))
                vertices = sortList(vertices, (depth % k))

                middle = len(vertices) // 2
                
                return BT(
                    value = vertices[middle],
                    left = build(
                        vertices=vertices[:middle],
                        depth=depth+1,
                    ),
                    right = build(
                        vertices=vertices[middle+1:],
                        depth=depth+1,
                    ),
                )

            return build(vertices=list(vertices), depth=0)
        
        NNRecord = collections.namedtuple("NNRecord", ["vertex", "distance"])
        NNRecord.__doc__ = """
        Used to keep track of the current best guess during a nearest
        neighbor search.
        """

        def find_nearest_neighbor(*, tree, vertex):
            """Find the nearest neighbor in a k-d tree for a given vertex.
            """
            k = 3 # Forcing k to be 3 dimensional
            best = None
            def search(*, tree, depth):
                """Recursively search through the k-d tree to find the nearest neighbor.
                """
                nonlocal best

                if tree is None:
                    return
                distance = SED(tree.value, vertex)
                if best is None or distance < best.distance:
                    best = NNRecord(vertex=tree.value, distance=distance)

                axis = depth % k
                diff = itemAtIndex(vertex,axis) - itemAtIndex(tree.value,axis)
                if diff <= 0:
                    close, away = tree.left, tree.right
                else:
                    close, away = tree.right, tree.left

                search(tree=close, depth=depth+1)
                if diff**2 < best.distance:
                    search(tree=away, depth=depth+1)

            search(tree=tree, depth=0)
            return best.vertex
        
        if useKDTree:
            tree = kdtree(topology)
            return find_nearest_neighbor(tree=tree, vertex=vertex)
        else:
            vertices = []
            _ = topology.Vertices(None, vertices)
            distances = []
            indices = []
            for i in range(len(vertices)):
                distances.append(SED(vertex, vertices[i]))
                indices.append(i)
            sorted_indices = [x for _, x in sorted(zip(distances, indices))]
        return vertices[sorted_indices[0]]

    @staticmethod
    def Origin():
        """
        Returns a vertex with coordinates (0, 0, 0)

        Parameters
        -----------

        Return
        -----------
        topologic_core.Vertex
        """
        return Vertex.ByCoordinates(0, 0, 0)
    
    @staticmethod
    def PerpendicularDistance(vertex, face, mantissa: int = 6):
        """
        Returns the perpendicular distance between the input vertex and the input face. The face is considered to be infinite.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        face : topologic_core.Face
            The input face.
        mantissa: int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The distance between the input vertex and the input topology.

        """
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology
        import math

        def distance_point_to_line(point, line_start, line_end):
            # Convert input points to NumPy arrays for vector operations
            point = np.array(point)
            line_start = np.array(line_start)
            line_end = np.array(line_end)
            
            # Calculate the direction vector of the edge
            line_direction = line_end - line_start
            
            # Vector from the edge's starting point to the point
            point_to_start = point - line_start
            
            # Calculate the parameter 't' where the projection of the point onto the edge occurs
            if np.dot(line_direction, line_direction) == 0:
                t = 0
            else:
                t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
            
            # Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
            if t < 0:
                return np.linalg.norm(point - line_start)
            elif t > 1:
                return np.linalg.norm(point - line_end)
            
            # Calculate the closest point on the edge to the given point
            closest_point = line_start + t * line_direction
            
            # Calculate the distance between the closest point and the given point
            distance = np.linalg.norm(point - closest_point)
            
            return distance
        if not Topology.IsInstance(vertex, "Vertex"):
            print("Vertex.PerpendicularDistance - Error: The input vertex is not a valid topologic vertex. Returning None.")
            return None
        if not Topology.IsInstance(face, "Face"):
            print("Vertex.PerpendicularDistance - Error: The input face is not a valid topologic face. Returning None.")
            return None
        dic = Face.PlaneEquation(face)
        if dic == None: # The face is degenerate. Try to treat as an edge.
            point = Vertex.Coordinates(vertex)
            face_vertices = Topology.Vertices(face)
            line_start = Vertex.Coordinates(face_vertices[0])
            line_end = Vertex.Coordinates(face_vertices[1])
            return round(distance_point_to_line(point, line_start, line_end), mantissa)
        a = dic["a"]
        b = dic["b"]
        c = dic["c"]
        d = dic["d"]
        x1, y1, z1 = Vertex.Coordinates(vertex)
        d = abs((a * x1 + b * y1 + c * z1 + d))
        e = (math.sqrt(a * a + b * b + c * c))
        if e == 0:
            return 0
        return round(d/e, mantissa)
    
    @staticmethod
    def PlaneEquation(vertices, mantissa: int = 6):
        """
        Returns the equation of the average plane passing through a list of vertices.

        Parameters
        -----------
        vertices : list
            The input list of vertices
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Return
        -----------
        dict
            The dictionary containing the values of a, b, c, d for the plane equation in the form of ax+by+cz+d=0.
            The keys in the dictionary are ["a", "b", "c". "d"]
        """

        vertices = [Vertex.Coordinates(v) for v in vertices]
        # Convert vertices to a NumPy array for easier calculations
        vertices = np.array(vertices)

        # Calculate the centroid of the vertices
        centroid = np.mean(vertices, axis=0)

        # Center the vertices by subtracting the centroid
        centered_vertices = vertices - centroid

        # Calculate the covariance matrix
        covariance_matrix = np.dot(centered_vertices.T, centered_vertices)

        # Find the normal vector by computing the eigenvector of the smallest eigenvalue
        _, eigen_vectors = np.linalg.eigh(covariance_matrix)
        normal_vector = eigen_vectors[:, 0]

        # Normalize the normal vector
        normal_vector /= np.linalg.norm(normal_vector)

        # Calculate the constant D using the centroid and the normal vector
        d = -np.dot(normal_vector, centroid)
        d = round(d, mantissa)

        # Create the plane equation in the form Ax + By + Cz + D = 0
        a, b, c = normal_vector
        a = round(a, mantissa)
        b = round(b, mantissa)
        c = round(c, mantissa)

        return {"a":a, "b":b, "c":c, "d":d}
    
    @staticmethod
    def Point(x=0, y=0, z=0):
        """
        Creates a point (vertex) using the input parameters

        Parameters
        -----------
        x : float , optional.
            The desired x coordinate. The default is 0.
        y : float , optional.
            The desired y coordinate. The default is 0.
        z : float , optional.
            The desired z coordinate. The default is 0.

        Return
        -----------
        topologic_core.Vertex
        """
        
        return Vertex.ByCoordinates(x, y, z)

    @staticmethod
    def Project(vertex, face, direction: bool = None, mantissa: int = 6):
        """
        Returns a vertex that is the projection of the input vertex unto the input face.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex to project unto the input face.
        face : topologic_core.Face
            The input face that receives the projection of the input vertex.
        direction : vector, optional
            The direction in which to project the input vertex unto the input face. If not specified, the direction of the projection is the normal of the input face. The default is None.
        mantissa : int , optional
            The length of the desired mantissa. The default is 6.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        topologic_core.Vertex
            The projected vertex.

        """
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology
        
        def project_point_onto_plane(point, plane_coeffs, direction_vector):
            """
            Project a 3D point onto a plane defined by its coefficients and using a direction vector.

            Parameters:
                point (tuple or list): The 3D point coordinates (x, y, z).
                plane_coeffs (tuple or list): The coefficients of the plane equation (a, b, c, d).
                direction_vector (tuple or list): The direction vector (vx, vy, vz).

            Returns:
                tuple: The projected point coordinates (x_proj, y_proj, z_proj).
            """
            # Unpack point coordinates
            x, y, z = point

            # Unpack plane coefficients
            a, b, c, d = plane_coeffs

            # Unpack direction vector
            vx, vy, vz = direction_vector

            # Calculate the distance from the point to the plane
            distance = (a * x + b * y + c * z + d) / (a * vx + b * vy + c * vz)

            # Calculate the projected point coordinates
            x_proj = x - distance * vx
            y_proj = y - distance * vy
            z_proj = z - distance * vz

            return [x_proj, y_proj, z_proj]

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        if not Topology.IsInstance(face, "Face"):
            return None
        eq = Face.PlaneEquation(face, mantissa= mantissa)
        if direction == None or direction == []:
            direction = Face.Normal(face)
        pt = project_point_onto_plane(Vertex.Coordinates(vertex), [eq["a"], eq["b"], eq["c"], eq["d"]], direction)
        return Vertex.ByCoordinates(pt[0], pt[1], pt[2])

    @staticmethod
    def X(vertex, mantissa: int = 6) -> float:
        """
        Returns the X coordinate of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The X coordinate of the input vertex.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        return round(vertex.X(), mantissa)

    @staticmethod
    def Y(vertex, mantissa: int = 6) -> float:
        """
        Returns the Y coordinate of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The Y coordinate of the input vertex.

        """
        from topologicpy.Topology import Topology
        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        return round(vertex.Y(), mantissa)

    @staticmethod
    def Z(vertex, mantissa: int = 6) -> float:
        """
        Returns the Z coordinate of the input vertex.

        Parameters
        ----------
        vertex : topologic_core.Vertex
            The input vertex.
        mantissa : int , optional
            The desired length of the mantissa. The default is 6.

        Returns
        -------
        float
            The Z coordinate of the input vertex.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(vertex, "Vertex"):
            return None
        return round(vertex.Z(), mantissa)

Static methods

def AreCollinear(vertices: list, tolerance: float = 0.0001)

Returns True if the input list of vertices form a straight line. Returns False otherwise.

Parameters

vertices : list
The input list of vertices.
tolerance : float, optional
The desired tolerance. The default is 0.0001.

Returns

bool
True if the input vertices are on the same side of the face. False otherwise.
Expand source code
@staticmethod
def AreCollinear(vertices: list, tolerance: float = 0.0001):
    """
    Returns True if the input list of vertices form a straight line. Returns False otherwise.

    Parameters
    ----------
    vertices : list
        The input list of vertices.
    tolerance : float, optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    bool
        True if the input vertices are on the same side of the face. False otherwise.

    """
    from topologicpy.Cluster import Cluster
    from topologicpy.Topology import Topology
    from topologicpy.Vector import Vector
    import sys
    def areCollinear(vertices, tolerance=0.0001):
        point1 = [Vertex.X(vertices[0]), Vertex.Y(vertices[0]), Vertex.Z(vertices[0])]
        point2 = [Vertex.X(vertices[1]), Vertex.Y(vertices[1]), Vertex.Z(vertices[1])]
        point3 = [Vertex.X(vertices[2]), Vertex.Y(vertices[2]), Vertex.Z(vertices[2])]

        vector1 = [point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]]
        vector2 = [point3[0] - point1[0], point3[1] - point1[1], point3[2] - point1[2]]

        cross_product_result = Vector.Cross(vector1, vector2, tolerance=tolerance)
        return cross_product_result == None
    
    if not isinstance(vertices, list):
        print("Vertex.AreCollinear - Error: The input list of vertices is not a valid list. Returning None.")
        return None
    vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
    if len(vertexList) < 2:
        print("Vertex.AreCollinear - Error: The input list of vertices does not contain sufficient valid vertices. Returning None.")
        return None
    if len(vertexList) < 3:
        return True # Any two vertices can form a line!
    cluster = Topology.SelfMerge(Cluster.ByTopologies(vertexList), tolerance=tolerance)
    vertexList = Topology.Vertices(cluster)
    slices = []
    for i in range(2,len(vertexList)):
        slices.append([vertexList[0], vertexList[1], vertexList[i]])
    for slice in slices:
        if not areCollinear(slice, tolerance=tolerance):
            return False
    return True
def AreIpsilateral(vertices: list, face) ‑> bool

Returns True if the input list of vertices are on one side of a face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

Parameters

vertices : list
The input list of vertices.
face : topologic_core.Face
The input face

Returns

bool
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
Expand source code
@staticmethod
def AreIpsilateral(vertices: list, face) -> bool:
    """
    Returns True if the input list of vertices are on one side of a face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

    Parameters
    ----------
    vertices : list
        The input list of vertices.
    face : topologic_core.Face
        The input face

    Returns
    -------
    bool
        True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

    """
    def check(dot_productA, pointB, pointC, normal):
        # Calculate the dot products of the vectors from the surface point to each of the input points.
        dot_productB = (pointB[0] - pointC[0]) * normal[0] + \
                    (pointB[1] - pointC[1]) * normal[1] + \
                    (pointB[2] - pointC[2]) * normal[2]

        # Check if both points are on the same side of the surface.
        if dot_productA * dot_productB > 0:
            return True

        # Check if both points are on opposite sides of the surface.
        elif dot_productA * dot_productB < 0:
            return False

        # Otherwise, at least one point is on the surface.
        else:
            return True

    from topologicpy.Vertex import Vertex
    from topologicpy.Face import Face
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(face, "Face"):
        return None
    vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
    if len(vertexList) < 2:
        return None
    pointA = Vertex.Coordinates(vertexList[0])
    pointC = Vertex.Coordinates(Face.VertexByParameters(face, 0.5, 0.5))
    normal = Face.Normal(face)
    dot_productA = (pointA[0] - pointC[0]) * normal[0] + \
                    (pointA[1] - pointC[1]) * normal[1] + \
                    (pointA[2] - pointC[2]) * normal[2]
    for i in range(1, len(vertexList)):
        pointB = Vertex.Coordinates(vertexList[i])
        if not check(dot_productA, pointB, pointC, normal):
            return False
    return True
def AreIpsilateralCluster(cluster, face) ‑> bool

Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

Parameters

cluster : topologic_core.Cluster
The input list of vertices.
face : topologic_core.Face
The input face

Returns

bool
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
Expand source code
@staticmethod
def AreIpsilateralCluster(cluster, face) -> bool:
    """
    Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input list of vertices.
    face : topologic_core.Face
        The input face

    Returns
    -------
    bool
        True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

    """
    from topologicpy.Topology import Topology
    if not Topology.IsInstance(cluster, "Topology"):
        return None
    vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
    return Vertex.AreIpsilateral(vertices, face)
def AreOnSameSide(vertices: list, face) ‑> bool

Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

Parameters

vertices : list
The input list of vertices.
face : topologic_core.Face
The input face

Returns

bool
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
Expand source code
@staticmethod
def AreOnSameSide(vertices: list, face) -> bool:
    """
    Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

    Parameters
    ----------
    vertices : list
        The input list of vertices.
    face : topologic_core.Face
        The input face

    Returns
    -------
    bool
        True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

    """
    return Vertex.AreIpsilateral(vertices, face)
def AreOnSameSideCluster(cluster, face) ‑> bool

Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

Parameters

cluster : topologic_core.Cluster
The input list of vertices.
face : topologic_core.Face
The input face

Returns

bool
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
Expand source code
@staticmethod
def AreOnSameSideCluster(cluster, face) -> bool:
    """
    Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input list of vertices.
    face : topologic_core.Face
        The input face

    Returns
    -------
    bool
        True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.

    """
    from topologicpy.Topology import Topology
    if not Topology.IsInstance(cluster, "Topology"):
        return None
    vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
    return Vertex.AreIpsilateral(vertices, face)
def ByCoordinates(*args, **kwargs)

Creates a vertex at the coordinates specified by the x, y, z inputs. You can call this method using a list of coordinates or individually. Examples: v = Vertex.ByCoordinates(3.4, 5.7, 2.8) v = Vertex.ByCoordinates([3.4, 5.7, 2.8]) v = Vertex.ByCoordinates(x=3.4, y=5.7, z=2.8)

Parameters

x : float , optional
The X coordinate. The default is 0.
y : float , optional
The Y coordinate. The default is 0.
z : float , optional
The Z coordinate. The defaults is 0.

Returns

topologic_core.Vertex
The created vertex.
Expand source code
@staticmethod
def ByCoordinates(*args, **kwargs):
    """
    Creates a vertex at the coordinates specified by the x, y, z inputs. You can call this method using a list of coordinates or individually.
    Examples:
    v = Vertex.ByCoordinates(3.4, 5.7, 2.8)
    v = Vertex.ByCoordinates([3.4, 5.7, 2.8])
    v = Vertex.ByCoordinates(x=3.4, y=5.7, z=2.8)

    Parameters
    ----------
    x : float , optional
        The X coordinate. The default is 0.
    y : float , optional
        The Y coordinate. The default is 0.
    z : float , optional
        The Z coordinate. The defaults is 0.

    Returns
    -------
    topologic_core.Vertex
        The created vertex.

    """
    import numbers
    x = None
    y = None
    z = None
    if len(args) > 3 or len(kwargs.items()) > 3:
        print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
        return None
    if len(args) > 0:
        value = args[0]
        if isinstance(value, list) and len(value) > 3:
            print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
            return None
        elif isinstance(value, list) and len(value) == 3:
            x = value[0]
            y = value[1]
            z = value[2]
        elif isinstance(value, list) and len(value) == 2:
            x = value[0]
            y = value[1]
        elif isinstance(value, list) and len(value) == 1:
            x = value[0]
        elif len(args) == 3:
            x = args[0]
            y = args[1]
            z = args[2]
        elif len(args) == 2:
            x = args[0]
            y = args[1]
        elif len(args) == 1:
            x = args[0]
    for key, value in kwargs.items():
        if "x" in key.lower():
            if not x == None:
                print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                return None
            x = value
        elif "y" in key.lower():
            if not y == None:
                print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                return None
            y = value
        elif "z" in key.lower():
            if not z == None:
                print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
                return None
            z = value
    if x == None:
        x = 0
    if y == None:
        y = 0
    if z == None:
        z = 0
    if not isinstance(x, numbers.Number):
        print("Vertex.ByCoordinates - Error: The x value is not a valid number. Returning None.")
        return None
    if not isinstance(y, numbers.Number):
        print("Vertex.ByCoordinates - Error: The y value is not a valid number. Returning None.")
        return None
    if not isinstance(z, numbers.Number):
        print("Vertex.ByCoordinates - Error: The z value is not a valid number. Returning None.")
        return None
    
    vertex = None
    try:
        vertex = topologic.Vertex.ByCoordinates(x, y, z) # Hook to Core
    except:
        vertex = None
        print("Vertex.ByCoordinates - Error: Could not create a topologic vertex. Returning None.")
    return vertex
def Centroid(vertices)

Returns the centroid of the input list of vertices.

Parameters

vertices : list
The input list of vertices

Return

topologic_core.Vertex The computed centroid of the input list of vertices

Expand source code
@staticmethod
def Centroid(vertices):
    """
    Returns the centroid of the input list of vertices.

    Parameters
    -----------
    vertices : list
        The input list of vertices

    Return
    ----------
    topologic_core.Vertex
        The computed centroid of the input list of vertices
    
    """
    from topologicpy.Topology import Topology

    if not isinstance(vertices, list):
        print("Vertex.Centroid - Error: The input vertices parameter is not a valid list. Returning None.")
        return None
    vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
    if len(vertices) < 1:
        print("Vertex.Centroid - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
        return None
    if len(vertices) == 1:
        return vertices[0]
    cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
    cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)
    cz = sum(Vertex.Z(v) for v in vertices) / len(vertices)
    return Vertex.ByCoordinates(cx, cy, cz)
def Clockwise2D(vertices)

Sorts the input list of vertices in a clockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

Parameters

vertices : list
The input list of vertices

Return

list The input list of vertices sorted in a counter clockwise fashion

Expand source code
@staticmethod
def Clockwise2D(vertices):
    """
    Sorts the input list of vertices in a clockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

    Parameters
    -----------
    vertices : list
        The input list of vertices

    Return
    -----------
    list
        The input list of vertices sorted in a counter clockwise fashion
    
    """
    return list(reversed(Vertex.CounterClockwise2D(vertices)))
def Coordinates(vertex, outputType: str = 'xyz', mantissa: int = 6) ‑> list

Returns the coordinates of the input vertex.

Parameters

vertex : topologic_core.Vertex
The input vertex.
outputType : string, optional
The desired output type. Could be any permutation or substring of "xyz" or the string "matrix". The default is "xyz". The input is case insensitive and the coordinates will be returned in the specified order.
mantissa : int , optional
The desired length of the mantissa. The default is 6.

Returns

list
The coordinates of the input vertex.
Expand source code
@staticmethod
def Coordinates(vertex, outputType: str = "xyz", mantissa: int = 6) -> list:
    """
    Returns the coordinates of the input vertex.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    outputType : string, optional
        The desired output type. Could be any permutation or substring of "xyz" or the string "matrix". The default is "xyz". The input is case insensitive and the coordinates will be returned in the specified order.
    mantissa : int , optional
        The desired length of the mantissa. The default is 6.

    Returns
    -------
    list
        The coordinates of the input vertex.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(vertex, "Vertex"):
        return None
    x = round(vertex.X(), mantissa)
    y = round(vertex.Y(), mantissa)
    z = round(vertex.Z(), mantissa)
    matrix = [[1, 0, 0, x],
            [0, 1, 0, y],
            [0, 0, 1, z],
            [0, 0, 0, 1]]
    output = []
    outputType = outputType.lower()
    if outputType == "matrix":
        return matrix
    else:
        outputType = list(outputType)
        for axis in outputType:
            if axis == "x":
                output.append(x)
            elif axis == "y":
                output.append(y)
            elif axis == "z":
                output.append(z)
    return output
def CounterClockwise2D(vertices)

Sorts the input list of vertices in a counterclockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

Parameters

vertices : list
The input list of vertices

Return

list The input list of vertices sorted in a counter clockwise fashion

Expand source code
@staticmethod
def CounterClockwise2D(vertices):
    """
    Sorts the input list of vertices in a counterclockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.

    Parameters
    -----------
    vertices : list
        The input list of vertices

    Return
    -----------
    list
        The input list of vertices sorted in a counter clockwise fashion
    
    """
    import math
    # find the centroid of the points
    cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
    cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)

    # sort the points based on their angle with respect to the centroid
    vertices.sort(key=lambda v: (math.atan2(Vertex.Y(v) - cy, Vertex.X(v) - cx) + 2 * math.pi) % (2 * math.pi))
    return vertices
def Degree(vertex, hostTopology, topologyType: str = 'edge')

Returns the vertex degree (the number of super topologies connected to it). See https://en.wikipedia.org/wiki/Degree_(graph_theory).

Parameters

vertex : topologic_core.Vertex
The input vertex.
hostTopology : topologic_core.Topology
The input host topology in which to search for the connected super topologies.
topologyType : str , optional
The topology type to search for. This can be any of "edge", "wire", "face", "shell", "cell", "cellcomplex", "cluster". It is case insensitive. If set to None, the immediate supertopology type is searched for. The default is None.

Returns

int
The number of super topologies connected to this vertex
Expand source code
@staticmethod
def Degree(vertex, hostTopology, topologyType: str = "edge"):
    """
    Returns the vertex degree (the number of super topologies connected to it). See https://en.wikipedia.org/wiki/Degree_(graph_theory).

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    hostTopology : topologic_core.Topology
        The input host topology in which to search for the connected super topologies.
    topologyType : str , optional
        The topology type to search for. This can be any of "edge", "wire", "face", "shell", "cell", "cellcomplex", "cluster". It is case insensitive. If set to None, the immediate supertopology type is searched for. The default is None.

    Returns
    -------
    int
        The number of super topologies connected to this vertex

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(vertex, "Vertex"):
        print("Vertex.Degree - Error: The input vertex parameter is not a valid topologic vertex. Returning None.")
    if not Topology.IsInstance(hostTopology, "Topology"):
        print("Vertex.Degree - Error: The input hostTopology parameter is not a valid topologic topology. Returning None.")
    superTopologies = Topology.SuperTopologies(topology=vertex, hostTopology=hostTopology, topologyType=topologyType)
    return len(superTopologies)
def Distance(vertex, topology, includeCentroid: bool = True, mantissa: int = 6) ‑> float

Returns the distance between the input vertex and the input topology. This method returns the distance to the closest sub-topology in the input topology, optionally including its centroid.

Parameters

vertex : topologic_core.Vertex
The input vertex.
topology : topologic_core.Topology
The input topology.
includeCentroid : bool
If set to True, the centroid of the input topology will be considered in finding the nearest subTopology to the input vertex. The default is True.
mantissa : int , optional
The desired length of the mantissa. The default is 6.

Returns

float
The distance between the input vertex and the input topology.
Expand source code
@staticmethod
def Distance(vertex, topology, includeCentroid: bool =True,
             mantissa: int = 6) -> float:
    """
    Returns the distance between the input vertex and the input topology. This method returns the distance to the closest sub-topology in the input topology, optionally including its centroid.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    topology : topologic_core.Topology
        The input topology.
    includeCentroid : bool
        If set to True, the centroid of the input topology will be considered in finding the nearest subTopology to the input vertex. The default is True.
    mantissa : int , optional
        The desired length of the mantissa. The default is 6.
    
    Returns
    -------
    float
        The distance between the input vertex and the input topology.

    """
    from topologicpy.Edge import Edge
    from topologicpy.Face import Face
    from topologicpy.Topology import Topology
    import math

    def distance_point_to_point(point1, point2):
        # Convert input points to NumPy arrays
        point1 = np.array(point1)
        point2 = np.array(point2)
        
        # Calculate the Euclidean distance
        distance = np.linalg.norm(point1 - point2)
        
        return distance

    def distance_point_to_line(point, line_start, line_end):
        # Convert input points to NumPy arrays for vector operations
        point = np.array(point)
        line_start = np.array(line_start)
        line_end = np.array(line_end)
        
        # Calculate the direction vector of the edge
        line_direction = line_end - line_start
        
        # Vector from the edge's starting point to the point
        point_to_start = point - line_start
        
        # Calculate the parameter 't' where the projection of the point onto the edge occurs
        if np.dot(line_direction, line_direction) == 0:
            t = 0
        else:
            t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
        
        # Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
        if t < 0:
            return np.linalg.norm(point - line_start)
        elif t > 1:
            return np.linalg.norm(point - line_end)
        
        # Calculate the closest point on the edge to the given point
        closest_point = line_start + t * line_direction
        
        # Calculate the distance between the closest point and the given point
        distance = np.linalg.norm(point - closest_point)
        
        return distance
    
    def distance_to_vertex(vertexA, vertexB):
        a = (Vertex.X(vertexA), Vertex.Y(vertexA), Vertex.Z(vertexA))
        b = (Vertex.X(vertexB), Vertex.Y(vertexB), Vertex.Z(vertexB))
        return distance_point_to_point(a, b)
    
    def distance_to_edge(vertex, edge):
        a = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
        sv = Edge.StartVertex(edge)
        ev = Edge.EndVertex(edge)
        svp = (Vertex.X(sv), Vertex.Y(sv), Vertex.Z(sv))
        evp = (Vertex.X(ev), Vertex.Y(ev), Vertex.Z(ev))
        return distance_point_to_line(a,svp, evp)
    
    def distance_to_face(vertex, face, includeCentroid):
        v_proj = Vertex.Project(vertex, face, mantissa=mantissa)
        if not Vertex.IsInternal(v_proj, face):
            vertices = Topology.Vertices(topology)
            distances = [distance_to_vertex(vertex, v) for v in vertices]
            edges = Topology.Edges(topology)
            distances += [distance_to_edge(vertex, e) for e in edges]
            if includeCentroid:
                distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
            return min(distances)
        dic = Face.PlaneEquation(face)
        a = dic["a"]
        b = dic["b"]
        c = dic["c"]
        d = dic["d"]
        x1, y1, z1 = Vertex.Coordinates(vertex)
        d = abs((a * x1 + b * y1 + c * z1 + d))
        e = (math.sqrt(a * a + b * b + c * c))
        if e == 0:
            return 0
        return d/e
    if not Topology.IsInstance(vertex, "Vertex") or not Topology.IsInstance(topology, "Topology"):
        return None
    if Topology.IsInstance(topology, "Vertex"):
        return round(distance_to_vertex(vertex,topology), mantissa)
    elif Topology.IsInstance(topology, "Edge"):
        return round(distance_to_edge(vertex,topology), mantissa)
    elif Topology.IsInstance(topology, "Wire"):
        vertices = Topology.Vertices(topology)
        distances = [distance_to_vertex(vertex, v) for v in vertices]
        edges = Topology.Edges(topology)
        distances += [distance_to_edge(vertex, e) for e in edges]
        if includeCentroid:
            distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
        return round(min(distances), mantissa)
    elif Topology.IsInstance(topology, "Face"):
        vertices = Topology.Vertices(topology)
        distances = [distance_to_vertex(vertex, v) for v in vertices]
        edges = Topology.Edges(topology)
        distances += [distance_to_edge(vertex, e) for e in edges]
        distances.append(distance_to_face(vertex,topology, includeCentroid))
        if includeCentroid:
            distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
        return round(min(distances), mantissa)
    elif Topology.IsInstance(topology, "Shell") or Topology.IsInstance(topology, "Cell") or Topology.IsInstance(topology, "CellComplex") or Topology.IsInstance(topology, "Cluster"):
        vertices = Topology.Vertices(topology)
        distances = [distance_to_vertex(vertex, v) for v in vertices]
        edges = Topology.Edges(topology)
        distances += [distance_to_edge(vertex, e) for e in edges]
        faces = Topology.Faces(topology)
        distances += [distance_to_face(vertex, f, includeCentroid) for f in faces]
        if includeCentroid:
            distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
        return round(min(distances), mantissa)
    else:
        print("Vertex.Distance - Error: Could not recognize the input topology. Returning None.")
        return None
def EnclosingCell(vertex, topology, exclusive: bool = True, tolerance: float = 0.0001) ‑> list

Returns the list of Cells found in the input topology that enclose the input vertex.

Parameters

vertex : topologic_core.Vertex
The input vertex.
topology : topologic_core.Topology
The input topology.
exclusive : bool , optional
If set to True, return only the first found enclosing cell. The default is True.
tolerance : float , optional
The tolerance for computing if the input vertex is enclosed in a cell. The default is 0.0001.

Returns

list
The list of enclosing cells.
Expand source code
@staticmethod
def EnclosingCell(vertex, topology, exclusive: bool = True, tolerance: float = 0.0001) -> list:
    """
    Returns the list of Cells found in the input topology that enclose the input vertex.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    topology : topologic_core.Topology
        The input topology.
    exclusive : bool , optional
        If set to True, return only the first found enclosing cell. The default is True.
    tolerance : float , optional
        The tolerance for computing if the input vertex is enclosed in a cell. The default is 0.0001.

    Returns
    -------
    list
        The list of enclosing cells.

    """
    from topologicpy.Cell import Cell
    from topologicpy.Topology import Topology
    
    def boundingBox(cell):
        vertices = []
        _ = cell.Vertices(None, vertices)
        x = []
        y = []
        z = []
        for aVertex in vertices:
            x.append(aVertex.X())
            y.append(aVertex.Y())
            z.append(aVertex.Z())
        return ([min(x), min(y), min(z), max(x), max(y), max(z)])
    
    if Topology.IsInstance(topology, "Cell"):
        cells = [topology]
    elif Topology.IsInstance(topology, "Cluster") or Topology.IsInstance(topology, "CellComplex"):
        cells = []
        _ = topology.Cells(None, cells)
    else:
        return None
    if len(cells) < 1:
        return None
    enclosingCells = []
    for i in range(len(cells)):
        bbox = boundingBox(cells[i])
        if ((vertex.X() < bbox[0]) or (vertex.Y() < bbox[1]) or (vertex.Z() < bbox[2]) or (vertex.X() > bbox[3]) or (vertex.Y() > bbox[4]) or (vertex.Z() > bbox[5])) == False:
            if Vertex.IsInternal(vertex, cells[i], tolerance=tolerance):
                if exclusive:
                    return([cells[i]])
                else:
                    enclosingCells.append(cells[i])
    return enclosingCells
def Fuse(vertices: list, mantissa: int = 6, tolerance: float = 0.0001)

Returns a list of vertices where vertices within a specified tolerance distance are fused while retaining duplicates, ensuring that vertices with nearly identical coordinates are replaced by a single shared coordinate.

Parameters

vertices : list
The input list of topologic vertices.
mantissa : int , optional
The desired length of the mantissa for retrieving vertex coordinates. The default is 6.
tolerance : float , optional
The desired tolerance for computing if vertices need to be fused. Any vertices that are closer to each other than this tolerance will be fused. The default is 0.0001.

Returns

list The list of fused vertices. This list contains the same number of vertices and in the same order as the input list of vertices. However, the coordinates of these vertices have now been modified so that they are exactly the same with other vertices that are within the tolerance distance.

Expand source code
@staticmethod
def Fuse(vertices: list, mantissa: int = 6, tolerance: float = 0.0001):
    """
    Returns a list of vertices where vertices within a specified tolerance distance are fused while retaining duplicates, ensuring that vertices with nearly identical coordinates are replaced by a single shared coordinate.

    Parameters
    ----------
    vertices : list
        The input list of topologic vertices.
    mantissa : int , optional
        The desired length of the mantissa for retrieving vertex coordinates. The default is 6.
    tolerance : float , optional
        The desired tolerance for computing if vertices need to be fused. Any vertices that are closer to each other than this tolerance will be fused. The default is 0.0001.

     Returns
    -------
    list
        The list of fused vertices. This list contains the same number of vertices and in the same order as the input list of vertices. However, the coordinates
        of these vertices have now been modified so that they are exactly the same with other vertices that are within the tolerance distance.
    """
    from topologicpy.Topology import Topology
    import numpy as np

    def fuse_vertices(vertices, tolerance):
        fused_vertices = []
        merged_indices = {}

        for idx, vertex in enumerate(vertices):
            if idx in merged_indices:
                fused_vertices.append(fused_vertices[merged_indices[idx]])
                continue

            merged_indices[idx] = len(fused_vertices)
            fused_vertex = vertex
            for i in range(idx + 1, len(vertices)):
                if i in merged_indices:
                    continue

                other_vertex = vertices[i]
                distance = np.linalg.norm(np.array(vertex) - np.array(other_vertex))
                if distance < tolerance:
                    # Choose the coordinate with the least amount of decimal points
                    if count_decimal_points(other_vertex) < count_decimal_points(fused_vertex):
                        fused_vertex = other_vertex

                    merged_indices[i] = len(fused_vertices)

            fused_vertices.append(fused_vertex)

        return fused_vertices
    def count_decimal_points(vertex):
        # Count the number of decimal points in the coordinates
        decimals_list = []
        for coord in vertex:
            coord_str = str(coord)
            if '.' in coord_str:
                decimals_list.append(len(coord_str.split('.')[1]))
            elif 'e' in coord_str:
                decimals_list.append(int(coord_str.split('e')[1].replace('-','')))
        return max(decimals_list)


    if not isinstance(vertices, list):
        print("Vertex.Fuse - Error: The input vertices parameter is not a valid list. Returning None.")
        return None
    vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
    if len(vertices) == 0:
        print("Vertex.Fuse - Error: The input vertices parameter does not contain any valid topologic vertices. Returning None.")
        return None
    
    vertices = [(Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa), Vertex.Z(v, mantissa=mantissa)) for v in vertices]
    fused_vertices = fuse_vertices(vertices, tolerance)
    return_vertices = [Vertex.ByCoordinates(list(coord)) for coord in fused_vertices]
    return return_vertices
def Index(vertex, vertices: list, strict: bool = False, tolerance: float = 0.0001) ‑> int

Returns index of the input vertex in the input list of vertices

Parameters

vertex : topologic_core.Vertex
The input vertex.
vertices : list
The input list of vertices.
strict : bool , optional
If set to True, the vertex must be strictly identical to the one found in the list. Otherwise, a distance comparison is used. The default is False.
tolerance : float , optional
The tolerance for computing if the input vertex is identical to a vertex from the list. The default is 0.0001.

Returns

int
The index of the input vertex in the input list of vertices.
Expand source code
@staticmethod
def Index(vertex, vertices: list, strict: bool = False, tolerance: float = 0.0001) -> int:
    """
    Returns index of the input vertex in the input list of vertices

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    vertices : list
        The input list of vertices.
    strict : bool , optional
        If set to True, the vertex must be strictly identical to the one found in the list. Otherwise, a distance comparison is used. The default is False.
    tolerance : float , optional
        The tolerance for computing if the input vertex is identical to a vertex from the list. The default is 0.0001.

    Returns
    -------
    int
        The index of the input vertex in the input list of vertices.

    """
    from topologicpy.Topology import Topology
    if not Topology.IsInstance(vertex, "Vertex"):
        return None
    if not isinstance(vertices, list):
        return None
    vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
    if len(vertices) == 0:
        return None
    for i in range(len(vertices)):
        if strict:
            if Topology.IsSame(vertex, vertices[i]):
                return i
        else:
            d = Vertex.Distance(vertex, vertices[i])
            if d < tolerance:
                return i
    return None
def InterpolateValue(vertex, vertices, n=3, key='intensity', tolerance=0.0001)

Interpolates the value of the input vertex based on the values of the n nearest vertices.

Parameters

vertex : topologic_core.Vertex
The input vertex.
vertices : list
The input list of vertices.
n : int , optional
The maximum number of nearest vertices to consider. The default is 3.
key : str , optional
The key that holds the value to be interpolated in the dictionaries of the vertices. The default is "intensity".
tolerance : float , optional
The tolerance for computing if the input vertex is coincident with another vertex in the input list of vertices. The default is 0.0001.

Returns

topologic_core.vertex
The input vertex with the interpolated value stored in its dictionary at the key specified by the input key. Other keys and values in the dictionary are preserved.
Expand source code
@staticmethod
def InterpolateValue(vertex, vertices, n=3, key="intensity", tolerance=0.0001):
    """
    Interpolates the value of the input vertex based on the values of the *n* nearest vertices.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    vertices : list
        The input list of vertices.
    n : int , optional
        The maximum number of nearest vertices to consider. The default is 3.
    key : str , optional
        The key that holds the value to be interpolated in the dictionaries of the vertices. The default is "intensity".
    tolerance : float , optional
        The tolerance for computing if the input vertex is coincident with another vertex in the input list of vertices. The default is 0.0001.

    Returns
    -------
    topologic_core.vertex
        The input vertex with the interpolated value stored in its dictionary at the key specified by the input key. Other keys and values in the dictionary are preserved.

    """

    def interpolate_value(point, data_points, n, tolerance=0.0001):
        """
        Interpolates the value associated with a point in 3D by averaging the values of the n nearest points.
        The influence of the adjacent points is inversely proportional to their distance from the input point.

        Args:
            data_points (list): A list of tuples, each representing a data point in 3D space as (x, y, z, value).
                                The 'value' represents the value associated with that data point.
            point (tuple): A tuple representing the point in 3D space as (x, y, z) for which we want to interpolate a value.
            n (int): The number of nearest points to consider for interpolation.

        Returns:
            The interpolated value for the input point.
        """
        # Calculate the distances between the input point and all data points
        distances = [(distance(p[:3], point), p[3]) for p in data_points]

        # Sort the distances in ascending order
        sorted_distances = sorted(distances, key=lambda x: x[0])

        # Take the n nearest points
        nearest_points = sorted_distances[:n]

        n_p = nearest_points[0]
        n_d = n_p[0]
        if n_d < tolerance:
            return n_p[1]

        # Calculate the weights for each nearest point based on inverse distance

        weights = [(1/d[0], d[1]) for d in nearest_points]

        # Normalize the weights so they sum to 1
        total_weight = sum(w[0] for w in weights)
        normalized_weights = [(w[0]/total_weight, w[1]) for w in weights]

        # Interpolate the value as the weighted average of the nearest points
        interpolated_value = sum(w[0]*w[1] for w in normalized_weights)

        return interpolated_value

    def distance(point1, point2):
        """
        Calculates the Euclidean distance between two points in 3D space.

        Args:
            point1 (tuple): A tuple representing a point in 3D space as (x, y, z).
            point2 (tuple): A tuple representing a point in 3D space as (x, y, z).

        Returns:
            The Euclidean distance between the two points.
        """
        return ((point1[0]-point2[0])**2 + (point1[1]-point2[1])**2 + (point1[2]-point2[2])**2)**0.5
    
    from topologicpy.Topology import Topology
    from topologicpy.Dictionary import Dictionary

    if not Topology.IsInstance(vertex, "Vertex"):
        return None
    if not isinstance(vertices, list):
        return None
    
    vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")]
    if len(vertices) == 0:
        return None
    
    point = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
    data_points = []
    for v in vertices:
        d = Topology.Dictionary(v)
        value = Dictionary.ValueAtKey(d, key)
        if not value == None:
            if type(value) == int or type(value) == float:
                data_points.append((Vertex.X(v), Vertex.Y(v), Vertex.Z(v), value))
    if len(data_points) == 0:
        return None
    if n > len(data_points):
        n = len(data_points)
    value = interpolate_value(point, data_points, n, tolerance=0.0001)
    d = Topology.Dictionary(vertex)
    d = Dictionary.SetValueAtKey(d, key, value)
    vertex = Topology.SetDictionary(vertex, d)
    return vertex
def IsCoincident(vertexA, vertexB, tolerance: float = 0.0001, silent: bool = False) ‑> bool

Returns True if the input vertexA is coincident with the input vertexB. Returns False otherwise.

Parameters

vertexA : topologic_core.Vertex
The first input vertex.
vertexB : topologic_core.Vertex
The second input vertex.
tolerance : float , optional
The tolerance for computing if the input vertexA is coincident with the input vertexB. The default is 0.0001.

Returns

bool
True if the input vertexA is coincident with the input vertexB. False otherwise.
Expand source code
@staticmethod
def IsCoincident(vertexA, vertexB, tolerance: float = 0.0001, silent: bool = False) -> bool:
    """
    Returns True if the input vertexA is coincident with the input vertexB. Returns False otherwise.

    Parameters
    ----------
    vertexA : topologic_core.Vertex
        The first input vertex.
    vertexB : topologic_core.Vertex
        The second input vertex.
    tolerance : float , optional
        The tolerance for computing if the input vertexA is coincident with the input vertexB. The default is 0.0001.

    Returns
    -------
    bool
        True if the input vertexA is coincident with the input vertexB. False otherwise.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(vertexA, "Vertex"):
        if not silent:
            print("Vertex.IsCoincident - Error: The input vertexA parameter is not a valid vertex. Returning None.")
        return None
    if not Topology.IsInstance(vertexB, "Vertex"):
        if not silent:
            print("Vertex.IsICoincident - Error: The input vertexB parameter is not a valid vertex. Returning None.")
        return None
    return Vertex.IsInternal(vertexA, vertexB, tolerance=tolerance, silent=silent)
def IsExternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) ‑> bool

Returns True if the input vertex is external to the input topology. Returns False otherwise.

Parameters

vertex : topologic_core.Vertex
The input vertex.
topology : topologic_core.Topology
The input topology.
tolerance : float , optional
The tolerance for computing if the input vertex is external to the input topology. The default is 0.0001.
silent : bool , optional
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

Returns

bool
True if the input vertex is external to the input topology. False otherwise.
Expand source code
@staticmethod
def IsExternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
    """
    Returns True if the input vertex is external to the input topology. Returns False otherwise.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    topology : topologic_core.Topology
        The input topology.
    tolerance : float , optional
        The tolerance for computing if the input vertex is external to the input topology. The default is 0.0001.
    silent : bool , optional
        If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

    Returns
    -------
    bool
        True if the input vertex is external to the input topology. False otherwise.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(vertex, "Vertex"):
        if not silent:
            print("Vertex.IsExternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
        return None
    if not Topology.IsInstance(topology, "Topology"):
        if not silent:
            print("Vertex.IsExternal - Error: The input topology parameter is not a valid topology. Returning None.")
        return None
    return not (Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent) or Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent))
def IsInternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) ‑> bool

Returns True if the input vertex is inside the input topology. Returns False otherwise.

Parameters

vertex : topologic_core.Vertex
The input vertex.
topology : topologic_core.Topology
The input topology.
tolerance : float , optional
The tolerance for computing if the input vertex is internal to the input topology. The default is 0.0001.
silent : bool , optional
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

Returns

bool
True if the input vertex is internal to the input topology. False otherwise.
Expand source code
@staticmethod
def IsInternal(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
    """
    Returns True if the input vertex is inside the input topology. Returns False otherwise.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    topology : topologic_core.Topology
        The input topology.
    tolerance : float , optional
        The tolerance for computing if the input vertex is internal to the input topology. The default is 0.0001.
    silent : bool , optional
        If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

    Returns
    -------
    bool
        True if the input vertex is internal to the input topology. False otherwise.

    """
    from topologicpy.Edge import Edge
    from topologicpy.Wire import Wire
    from topologicpy.Face import Face
    from topologicpy.CellComplex import CellComplex
    from topologicpy.Cluster import Cluster
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(vertex, "Vertex"):
        if not silent:
            print("Vertex.IsInternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
        return None
    if not Topology.IsInstance(topology, "Topology"):
        if not silent:
            print("Vertex.IsInternal - Error: The input topology parameter is not a valid topology. Returning None.")
        return None

    if Topology.IsInstance(topology, "Vertex"):
        return Vertex.Distance(vertex, topology) < tolerance
    elif Topology.IsInstance(topology, "Edge"):
        try:
            parameter = Edge.ParameterAtVertex(topology, vertex)
        except:
            parameter = 400 #aribtrary large number greater than 1
        if parameter == None:
            return False
        return 0 <= parameter <= 1
    elif Topology.IsInstance(topology, "Wire"):
        vertices = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) > 1]
        edges = Wire.Edges(topology)
        sub_list = vertices + edges
        for sub in sub_list:
            if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                return True
        return False
    elif Topology.IsInstance(topology, "Face"):
        # Test the distance first
        if Vertex.PerpendicularDistance(vertex, topology) > tolerance:
            return False
        if Vertex.IsPeripheral(vertex, topology):
            return False
        normal = Face.Normal(topology)
        proj_v = Vertex.Project(vertex, topology)
        v1 = Topology.TranslateByDirectionDistance(proj_v, normal, 1)
        v2 = Topology.TranslateByDirectionDistance(proj_v, normal, -1)
        edge = Edge.ByVertices(v1, v2)
        intersect = edge.Intersect(topology)
        if intersect == None:
            return False
        return True
    elif Topology.IsInstance(topology, "Shell"):
        if Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent):
            return False
        else:
            edges = Topology.Edges(topology)
            for edge in edges:
                if Vertex.IsInternal(vertex, edge, tolerance=tolerance, silent=silent):
                    return True
            faces = Topology.Faces(topology)
            for face in faces:
                if Vertex.IsInternal(vertex, face, tolerance=tolerance, silent=silent):
                    return True
            return False
    elif Topology.IsInstance(topology, "Cell"):
        return topologic.CellUtility.Contains(topology, vertex, tolerance) == 0 # Hook to Core
    elif Topology.IsInstance(topology, "CellComplex"):
        ext_boundary = CellComplex.ExternalBoundary(topology)
        return Vertex.IsInternal(vertex, ext_boundary, tolerance=tolerance, silent=silent)
    elif Topology.IsInstance(topology, "Cluster"):
        sub_list = Cluster.FreeTopologies(topology)
        for sub in sub_list:
            if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                return True
        return False
    return False
def IsPeripheral(vertex, topology, tolerance: float = 0.0001, silent: bool = False) ‑> bool

Returns True if the input vertex is peripheral to the input topology. Returns False otherwise. A vertex is said to be peripheral to the input topology if: 01. Vertex: If it is internal to it (i.e. coincident with it). 02. Edge: If it is internal to its start or end vertices. 03. Manifold open wire: If it is internal to its start or end vertices. 04. Manifold closed wire: If it is internal to any of its vertices. 05. Non-manifold wire: If it is internal to any of its vertices that has a vertex degree of 1. 06. Face: If it is internal to any of its edges or vertices. 07. Shell: If it is internal to external boundary 08. Cell: If it is internal to any of its faces, edges, or vertices. 09. CellComplex: If it is peripheral to its external boundary. 10. Cluster: If it is peripheral to any of its free topologies. (See Cluster.FreeTopologies)

Parameters

vertex : topologic_core.Vertex
The input vertex.
topology : topologic_core.Topology
The input topology.
tolerance : float , optional
The tolerance for computing if the input vertex is peripheral to the input topology. The default is 0.0001.
silent : bool , optional
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

Returns

bool
True if the input vertex is peripheral to the input topology. False otherwise.
Expand source code
@staticmethod
def IsPeripheral(vertex, topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
    """
    Returns True if the input vertex is peripheral to the input topology. Returns False otherwise.
    A vertex is said to be peripheral to the input topology if:
        01. Vertex: If it is internal to it (i.e. coincident with it).
        02. Edge: If it is internal to its start or end vertices.
        03. Manifold open wire: If it is internal to its start or end vertices.
        04. Manifold closed wire: If it is internal to any of its vertices.
        05. Non-manifold wire: If it is internal to any of its vertices that has a vertex degree of 1.
        06. Face: If it is internal to any of its edges or vertices.
        07. Shell: If it is internal to external boundary
        08. Cell: If it is internal to any of its faces, edges, or vertices.
        09. CellComplex: If it is peripheral to its external boundary.
        10. Cluster: If it is peripheral to any of its free topologies. (See Cluster.FreeTopologies)

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    topology : topologic_core.Topology
        The input topology.
    tolerance : float , optional
        The tolerance for computing if the input vertex is peripheral to the input topology. The default is 0.0001.
    silent : bool , optional
        If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.

    Returns
    -------
    bool
        True if the input vertex is peripheral to the input topology. False otherwise.

    """
    from topologicpy.Edge import Edge
    from topologicpy.Wire import Wire
    from topologicpy.Face import Face
    from topologicpy.Shell import Shell
    from topologicpy.CellComplex import CellComplex
    from topologicpy.Cluster import Cluster
    from topologicpy.Topology import Topology
    
    if not Topology.IsInstance(vertex, "Vertex"):
        if not silent:
            print("Vertex.IsPeripheral - Error: The input vertex parameter is not a valid vertex. Returning None.")
        return None
    if not Topology.IsInstance(topology, "Topology"):
        if not silent:
            print("Vertex.IsPeripheral - Error: The input topology parameter is not a valid topology. Returning None.")
        return None

    if Topology.IsInstance(topology, "Vertex"):
        return Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent)
    elif Topology.IsInstance(topology, "Edge"):
        sv = Edge.StartVertex(topology)
        ev = Edge.EndVertex(topology)
        f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
        f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
        return f1 or f2
    elif Topology.IsInstance(topology, "Wire"):
        if Wire.IsManifold(topology):
            if not Wire.IsClosed(topology):
                sv = Wire.StartVertex(topology)
                ev = Wire.EndVertex(topology)
                f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
                f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
                return f1 or f2
            else:
                sub_list = [v for v in Topology.Vertices(topology)]
                for sub in sub_list:
                    if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                        return True
                return False
        else:
            sub_list = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) == 1]
            for sub in sub_list:
                if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                    return True
            return False
    elif Topology.IsInstance(topology, "Face"):
        sub_list = Topology.Vertices(topology) + Topology.Edges(topology)
        for sub in sub_list:
            if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                return True
        return False
    elif Topology.IsInstance(topology, "Shell"):
        ext_boundary = Shell.ExternalBoundary(topology)
        sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary)
        for sub in sub_list:
            if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                return True
        return False
    elif Topology.IsInstance(topology, "Cell"):
        sub_list = Topology.Vertices(topology) + Topology.Edges(topology) + Topology.Faces(topology)
        for sub in sub_list:
            if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                return True
        return False
    elif Topology.IsInstance(topology, "CellComplex"):
        ext_boundary = CellComplex.ExternalBoundary(topology)
        sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary) + Topology.Faces(ext_boundary)
        for sub in sub_list:
            if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
                return True
        return False
    elif Topology.IsInstance(topology, "Cluster"):
        sub_list = Cluster.FreeTopologies(topology)
        for sub in sub_list:
            if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
                return True
        return False
    return False
def NearestVertex(vertex, topology, useKDTree: bool = True)

Returns the vertex found in the input topology that is the nearest to the input vertex.

Parameters

vertex : topologic_core.Vertex
The input vertex.
topology : topologic_core.Topology
The input topology to be searched for the nearest vertex.
useKDTree : bool , optional
if set to True, the algorithm will use a KDTree method to search for the nearest vertex. The default is True.

Returns

topologic_core.Vertex
The nearest vertex.
Expand source code
@staticmethod
def NearestVertex(vertex, topology, useKDTree: bool = True):
    """
    Returns the vertex found in the input topology that is the nearest to the input vertex.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    topology : topologic_core.Topology
        The input topology to be searched for the nearest vertex.
    useKDTree : bool , optional
        if set to True, the algorithm will use a KDTree method to search for the nearest vertex. The default is True.

    Returns
    -------
    topologic_core.Vertex
        The nearest vertex.

    """
    from topologicpy.Topology import Topology

    def SED(a, b):
        """Compute the squared Euclidean distance between X and Y."""
        p1 = (a.X(), a.Y(), a.Z())
        p2 = (b.X(), b.Y(), b.Z())
        return sum((i-j)**2 for i, j in zip(p1, p2))
    
    BT = collections.namedtuple("BT", ["value", "left", "right"])
    BT.__doc__ = """
    A Binary Tree (BT) with a node value, and left- and
    right-subtrees.
    """
    def firstItem(v):
        return v.X()
    def secondItem(v):
        return v.Y()
    def thirdItem(v):
        return v.Z()

    def itemAtIndex(v, index):
        if index == 0:
            return v.X()
        elif index == 1:
            return v.Y()
        elif index == 2:
            return v.Z()

    def sortList(vertices, index):
        if index == 0:
            vertices.sort(key=firstItem)
        elif index == 1:
            vertices.sort(key=secondItem)
        elif index == 2:
            vertices.sort(key=thirdItem)
        return vertices
    
    def kdtree(topology):
        assert Topology.IsInstance(topology, "Topology"), "Vertex.NearestVertex: The input is not a Topology."
        vertices = []
        _ = topology.Vertices(None, vertices)
        assert (len(vertices) > 0), "Vertex.NearestVertex: Could not find any vertices in the input Topology"

        """Construct a k-d tree from an iterable of vertices.

        This algorithm is taken from Wikipedia. For more details,

        > https://en.wikipedia.org/wiki/K-d_tree#Construction

        """
        # k = len(points[0])
        k = 3

        def build(*, vertices, depth):
            if len(vertices) == 0:
                return None
            #points.sort(key=operator.itemgetter(depth % k))
            vertices = sortList(vertices, (depth % k))

            middle = len(vertices) // 2
            
            return BT(
                value = vertices[middle],
                left = build(
                    vertices=vertices[:middle],
                    depth=depth+1,
                ),
                right = build(
                    vertices=vertices[middle+1:],
                    depth=depth+1,
                ),
            )

        return build(vertices=list(vertices), depth=0)
    
    NNRecord = collections.namedtuple("NNRecord", ["vertex", "distance"])
    NNRecord.__doc__ = """
    Used to keep track of the current best guess during a nearest
    neighbor search.
    """

    def find_nearest_neighbor(*, tree, vertex):
        """Find the nearest neighbor in a k-d tree for a given vertex.
        """
        k = 3 # Forcing k to be 3 dimensional
        best = None
        def search(*, tree, depth):
            """Recursively search through the k-d tree to find the nearest neighbor.
            """
            nonlocal best

            if tree is None:
                return
            distance = SED(tree.value, vertex)
            if best is None or distance < best.distance:
                best = NNRecord(vertex=tree.value, distance=distance)

            axis = depth % k
            diff = itemAtIndex(vertex,axis) - itemAtIndex(tree.value,axis)
            if diff <= 0:
                close, away = tree.left, tree.right
            else:
                close, away = tree.right, tree.left

            search(tree=close, depth=depth+1)
            if diff**2 < best.distance:
                search(tree=away, depth=depth+1)

        search(tree=tree, depth=0)
        return best.vertex
    
    if useKDTree:
        tree = kdtree(topology)
        return find_nearest_neighbor(tree=tree, vertex=vertex)
    else:
        vertices = []
        _ = topology.Vertices(None, vertices)
        distances = []
        indices = []
        for i in range(len(vertices)):
            distances.append(SED(vertex, vertices[i]))
            indices.append(i)
        sorted_indices = [x for _, x in sorted(zip(distances, indices))]
    return vertices[sorted_indices[0]]
def Origin()

Returns a vertex with coordinates (0, 0, 0)

Parameters

Return

topologic_core.Vertex

Expand source code
@staticmethod
def Origin():
    """
    Returns a vertex with coordinates (0, 0, 0)

    Parameters
    -----------

    Return
    -----------
    topologic_core.Vertex
    """
    return Vertex.ByCoordinates(0, 0, 0)
def PerpendicularDistance(vertex, face, mantissa: int = 6)

Returns the perpendicular distance between the input vertex and the input face. The face is considered to be infinite.

Parameters

vertex : topologic_core.Vertex
The input vertex.
face : topologic_core.Face
The input face.
mantissa : int , optional
The desired length of the mantissa. The default is 6.

Returns

float
The distance between the input vertex and the input topology.
Expand source code
@staticmethod
def PerpendicularDistance(vertex, face, mantissa: int = 6):
    """
    Returns the perpendicular distance between the input vertex and the input face. The face is considered to be infinite.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    face : topologic_core.Face
        The input face.
    mantissa: int , optional
        The desired length of the mantissa. The default is 6.

    Returns
    -------
    float
        The distance between the input vertex and the input topology.

    """
    from topologicpy.Face import Face
    from topologicpy.Topology import Topology
    import math

    def distance_point_to_line(point, line_start, line_end):
        # Convert input points to NumPy arrays for vector operations
        point = np.array(point)
        line_start = np.array(line_start)
        line_end = np.array(line_end)
        
        # Calculate the direction vector of the edge
        line_direction = line_end - line_start
        
        # Vector from the edge's starting point to the point
        point_to_start = point - line_start
        
        # Calculate the parameter 't' where the projection of the point onto the edge occurs
        if np.dot(line_direction, line_direction) == 0:
            t = 0
        else:
            t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
        
        # Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
        if t < 0:
            return np.linalg.norm(point - line_start)
        elif t > 1:
            return np.linalg.norm(point - line_end)
        
        # Calculate the closest point on the edge to the given point
        closest_point = line_start + t * line_direction
        
        # Calculate the distance between the closest point and the given point
        distance = np.linalg.norm(point - closest_point)
        
        return distance
    if not Topology.IsInstance(vertex, "Vertex"):
        print("Vertex.PerpendicularDistance - Error: The input vertex is not a valid topologic vertex. Returning None.")
        return None
    if not Topology.IsInstance(face, "Face"):
        print("Vertex.PerpendicularDistance - Error: The input face is not a valid topologic face. Returning None.")
        return None
    dic = Face.PlaneEquation(face)
    if dic == None: # The face is degenerate. Try to treat as an edge.
        point = Vertex.Coordinates(vertex)
        face_vertices = Topology.Vertices(face)
        line_start = Vertex.Coordinates(face_vertices[0])
        line_end = Vertex.Coordinates(face_vertices[1])
        return round(distance_point_to_line(point, line_start, line_end), mantissa)
    a = dic["a"]
    b = dic["b"]
    c = dic["c"]
    d = dic["d"]
    x1, y1, z1 = Vertex.Coordinates(vertex)
    d = abs((a * x1 + b * y1 + c * z1 + d))
    e = (math.sqrt(a * a + b * b + c * c))
    if e == 0:
        return 0
    return round(d/e, mantissa)
def PlaneEquation(vertices, mantissa: int = 6)

Returns the equation of the average plane passing through a list of vertices.

Parameters

vertices : list
The input list of vertices
mantissa : int , optional
The desired length of the mantissa. The default is 6.

Return

dict The dictionary containing the values of a, b, c, d for the plane equation in the form of ax+by+cz+d=0. The keys in the dictionary are ["a", "b", "c". "d"]

Expand source code
@staticmethod
def PlaneEquation(vertices, mantissa: int = 6):
    """
    Returns the equation of the average plane passing through a list of vertices.

    Parameters
    -----------
    vertices : list
        The input list of vertices
    mantissa : int , optional
        The desired length of the mantissa. The default is 6.

    Return
    -----------
    dict
        The dictionary containing the values of a, b, c, d for the plane equation in the form of ax+by+cz+d=0.
        The keys in the dictionary are ["a", "b", "c". "d"]
    """

    vertices = [Vertex.Coordinates(v) for v in vertices]
    # Convert vertices to a NumPy array for easier calculations
    vertices = np.array(vertices)

    # Calculate the centroid of the vertices
    centroid = np.mean(vertices, axis=0)

    # Center the vertices by subtracting the centroid
    centered_vertices = vertices - centroid

    # Calculate the covariance matrix
    covariance_matrix = np.dot(centered_vertices.T, centered_vertices)

    # Find the normal vector by computing the eigenvector of the smallest eigenvalue
    _, eigen_vectors = np.linalg.eigh(covariance_matrix)
    normal_vector = eigen_vectors[:, 0]

    # Normalize the normal vector
    normal_vector /= np.linalg.norm(normal_vector)

    # Calculate the constant D using the centroid and the normal vector
    d = -np.dot(normal_vector, centroid)
    d = round(d, mantissa)

    # Create the plane equation in the form Ax + By + Cz + D = 0
    a, b, c = normal_vector
    a = round(a, mantissa)
    b = round(b, mantissa)
    c = round(c, mantissa)

    return {"a":a, "b":b, "c":c, "d":d}
def Point(x=0, y=0, z=0)

Creates a point (vertex) using the input parameters

Parameters

x : float , optional. The desired x coordinate. The default is 0. y : float , optional. The desired y coordinate. The default is 0. z : float , optional. The desired z coordinate. The default is 0.

Return

topologic_core.Vertex

Expand source code
@staticmethod
def Point(x=0, y=0, z=0):
    """
    Creates a point (vertex) using the input parameters

    Parameters
    -----------
    x : float , optional.
        The desired x coordinate. The default is 0.
    y : float , optional.
        The desired y coordinate. The default is 0.
    z : float , optional.
        The desired z coordinate. The default is 0.

    Return
    -----------
    topologic_core.Vertex
    """
    
    return Vertex.ByCoordinates(x, y, z)
def Project(vertex, face, direction: bool = None, mantissa: int = 6)

Returns a vertex that is the projection of the input vertex unto the input face.

Parameters

vertex : topologic_core.Vertex
The input vertex to project unto the input face.
face : topologic_core.Face
The input face that receives the projection of the input vertex.
direction : vector, optional
The direction in which to project the input vertex unto the input face. If not specified, the direction of the projection is the normal of the input face. The default is None.
mantissa : int , optional
The length of the desired mantissa. The default is 6.
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

topologic_core.Vertex
The projected vertex.
Expand source code
@staticmethod
def Project(vertex, face, direction: bool = None, mantissa: int = 6):
    """
    Returns a vertex that is the projection of the input vertex unto the input face.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex to project unto the input face.
    face : topologic_core.Face
        The input face that receives the projection of the input vertex.
    direction : vector, optional
        The direction in which to project the input vertex unto the input face. If not specified, the direction of the projection is the normal of the input face. The default is None.
    mantissa : int , optional
        The length of the desired mantissa. The default is 6.
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    topologic_core.Vertex
        The projected vertex.

    """
    from topologicpy.Face import Face
    from topologicpy.Topology import Topology
    
    def project_point_onto_plane(point, plane_coeffs, direction_vector):
        """
        Project a 3D point onto a plane defined by its coefficients and using a direction vector.

        Parameters:
            point (tuple or list): The 3D point coordinates (x, y, z).
            plane_coeffs (tuple or list): The coefficients of the plane equation (a, b, c, d).
            direction_vector (tuple or list): The direction vector (vx, vy, vz).

        Returns:
            tuple: The projected point coordinates (x_proj, y_proj, z_proj).
        """
        # Unpack point coordinates
        x, y, z = point

        # Unpack plane coefficients
        a, b, c, d = plane_coeffs

        # Unpack direction vector
        vx, vy, vz = direction_vector

        # Calculate the distance from the point to the plane
        distance = (a * x + b * y + c * z + d) / (a * vx + b * vy + c * vz)

        # Calculate the projected point coordinates
        x_proj = x - distance * vx
        y_proj = y - distance * vy
        z_proj = z - distance * vz

        return [x_proj, y_proj, z_proj]

    if not Topology.IsInstance(vertex, "Vertex"):
        return None
    if not Topology.IsInstance(face, "Face"):
        return None
    eq = Face.PlaneEquation(face, mantissa= mantissa)
    if direction == None or direction == []:
        direction = Face.Normal(face)
    pt = project_point_onto_plane(Vertex.Coordinates(vertex), [eq["a"], eq["b"], eq["c"], eq["d"]], direction)
    return Vertex.ByCoordinates(pt[0], pt[1], pt[2])
def X(vertex, mantissa: int = 6) ‑> float

Returns the X coordinate of the input vertex.

Parameters

vertex : topologic_core.Vertex
The input vertex.
mantissa : int , optional
The desired length of the mantissa. The default is 6.

Returns

float
The X coordinate of the input vertex.
Expand source code
@staticmethod
def X(vertex, mantissa: int = 6) -> float:
    """
    Returns the X coordinate of the input vertex.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    mantissa : int , optional
        The desired length of the mantissa. The default is 6.

    Returns
    -------
    float
        The X coordinate of the input vertex.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(vertex, "Vertex"):
        return None
    return round(vertex.X(), mantissa)
def Y(vertex, mantissa: int = 6) ‑> float

Returns the Y coordinate of the input vertex.

Parameters

vertex : topologic_core.Vertex
The input vertex.
mantissa : int , optional
The desired length of the mantissa. The default is 6.

Returns

float
The Y coordinate of the input vertex.
Expand source code
@staticmethod
def Y(vertex, mantissa: int = 6) -> float:
    """
    Returns the Y coordinate of the input vertex.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    mantissa : int , optional
        The desired length of the mantissa. The default is 6.

    Returns
    -------
    float
        The Y coordinate of the input vertex.

    """
    from topologicpy.Topology import Topology
    if not Topology.IsInstance(vertex, "Vertex"):
        return None
    return round(vertex.Y(), mantissa)
def Z(vertex, mantissa: int = 6) ‑> float

Returns the Z coordinate of the input vertex.

Parameters

vertex : topologic_core.Vertex
The input vertex.
mantissa : int , optional
The desired length of the mantissa. The default is 6.

Returns

float
The Z coordinate of the input vertex.
Expand source code
@staticmethod
def Z(vertex, mantissa: int = 6) -> float:
    """
    Returns the Z coordinate of the input vertex.

    Parameters
    ----------
    vertex : topologic_core.Vertex
        The input vertex.
    mantissa : int , optional
        The desired length of the mantissa. The default is 6.

    Returns
    -------
    float
        The Z coordinate of the input vertex.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(vertex, "Vertex"):
        return None
    return round(vertex.Z(), mantissa)