Module Wire
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/>.
from binascii import a2b_base64
from re import A
import topologic_core as topologic
from topologicpy.Topology import Topology
import math
import itertools
class Wire(Topology):
@staticmethod
def Arc(startVertex, middleVertex, endVertex, sides: int = 16, close: bool = True, tolerance: float = 0.0001):
"""
Creates an arc. The base chord will be parallel to the x-axis and the height will point in the positive y-axis direction.
Parameters
----------
startVertex : topologic_core.Vertex
The location of the start vertex of the arc.
middleVertex : topologic_core.Vertex
The location of the middle vertex (apex) of the arc.
endVertex : topologic_core.Vertex
The location of the end vertex of the arc.
sides : int , optional
The number of sides of the arc. The default is 16.
close : bool , optional
If set to True, the arc will be closed by connecting the last vertex to the first vertex. Otherwise, it will be left open.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created arc.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Face import Face
from topologicpy.Topology import Topology
def segmented_arc(x1, y1, x2, y2, x3, y3, sides):
import math
"""
Generates a segmented arc passing through the three given points.
Arguments:
x1, y1: Coordinates of the first point
x2, y2: Coordinates of the second point
x3, y3: Coordinates of the third point
sides: Number of sides to divide the arc
Returns:
List of tuples [x, y] representing the segmented arc passing through the points
"""
# Calculate the center of the circle
A = x2 - x1
B = y2 - y1
C = x3 - x1
D = y3 - y1
E = A * (x1 + x2) + B * (y1 + y2)
F = C * (x1 + x3) + D * (y1 + y3)
G = 2 * (A * (y3 - y2) - B * (x3 - x2))
if G == 0:
center_x = 0
center_y = 0
else:
center_x = (D * E - B * F) / G
center_y = (A * F - C * E) / G
# Calculate the radius of the circle
radius = math.sqrt((center_x - x1) ** 2 + (center_y - y1) ** 2)
# Calculate the angles between the center and the three points
angle1 = math.atan2(y1 - center_y, x1 - center_x)
angle3 = math.atan2(y3 - center_y, x3 - center_x)
# Calculate the angle between points 1 and 3
angle13 = (angle3 - angle1) % (2 * math.pi)
if angle13 < 0:
angle13 += 2 * math.pi
# Determine the direction of the arc based on the angle between points 1 and 3
if angle13 < math.pi:
start_angle = angle3
end_angle = angle1
else:
start_angle = angle1
end_angle = angle3
# Calculate the angle increment
angle_increment = (end_angle - start_angle) / sides
# Generate the points of the arc passing through the points
arc_points = []
for i in range(sides + 1):
angle = start_angle + i * angle_increment
x = center_x + radius * math.cos(angle)
y = center_y + radius * math.sin(angle)
arc_points.append([x, y])
return arc_points
if not Topology.IsInstance(startVertex, "Vertex"):
print("Wire.Arc - Error: The startVertex parameter is not a valid vertex. Returning None.")
return None
if not Topology.IsInstance(middleVertex, "Vertex"):
print("Wire.Arc - Error: The middleVertex parameter is not a valid vertex. Returning None.")
return None
if not Topology.IsInstance(endVertex, "Vertex"):
print("Wire.Arc - Error: The endVertex parameter is not a valid vertex. Returning None.")
return None
if Vertex.AreCollinear([startVertex, middleVertex, endVertex], tolerance = tolerance):
return Wire.ByVertices([startVertex, middleVertex, endVertex], close=False)
w = Wire.ByVertices([startVertex, middleVertex, endVertex], close=True)
f = Face.ByWire(w, tolerance=tolerance)
normal = Face.Normal(f)
flat_w = Topology.Flatten(w, origin=startVertex, direction=normal)
v1, v2, v3 = Topology.Vertices(flat_w)
x1, y1, z1 = Vertex.Coordinates(v1)
x2, y2, z2 = Vertex.Coordinates(v2)
x3, y3, z3 = Vertex.Coordinates(v3)
arc_points = segmented_arc(x1, y1, x2, y2, x3, y3, sides)
arc_verts = [Vertex.ByCoordinates(coord[0], coord[1], 0) for coord in arc_points]
arc = Wire.ByVertices(arc_verts, close=close)
# Unflatten the arc
arc = Topology.Unflatten(arc, origin=startVertex, direction=normal)
return arc
@staticmethod
def BoundingRectangle(topology, optimize: int = 0, tolerance=0.0001):
"""
Returns a wire representing a bounding rectangle of the input topology. The returned wire contains a dictionary with key "zrot" that represents rotations around the Z axis. If applied the resulting wire will become axis-aligned.
Parameters
----------
topology : topologic_core.Topology
The input topology.
optimize : int , optional
If set to an integer from 1 (low optimization) to 10 (high optimization), the method will attempt to optimize the bounding rectangle so that it reduces its surface area. The default is 0 which will result in an axis-aligned bounding rectangle. The default is 0.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The bounding rectangle of the input topology.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Topology import Topology
from topologicpy.Dictionary import Dictionary
from random import sample
import time
def br(topology):
vertices = []
_ = topology.Vertices(None, vertices)
x = []
y = []
for aVertex in vertices:
x.append(aVertex.X())
y.append(aVertex.Y())
minX = min(x)
minY = min(y)
maxX = max(x)
maxY = max(y)
return [minX, minY, maxX, maxY]
if not Topology.IsInstance(topology, "Topology"):
return None
world_origin = Vertex.Origin()
vertices = Topology.SubTopologies(topology=topology, subTopologyType="vertex")
start = time.time()
period = 0
result = True
while result and period < 30:
vList = sample(vertices, 3)
result = Vertex.AreCollinear(vList)
end = time.time()
period = end - start
if result == True:
print("Wire.BoundingRectangle - Error: Could not find three vertices that are not colinear within 30 seconds. Returning None.")
return None
w = Wire.ByVertices(vList)
f = Face.ByWire(w, tolerance=tolerance)
f_origin = Topology.Centroid(f)
normal = Face.Normal(f)
topology = Topology.Flatten(topology, origin=f_origin, direction=normal)
boundingRectangle = br(topology)
minX = boundingRectangle[0]
minY = boundingRectangle[1]
maxX = boundingRectangle[2]
maxY = boundingRectangle[3]
w = abs(maxX - minX)
l = abs(maxY - minY)
best_area = l*w
orig_area = best_area
best_z = 0
best_br = boundingRectangle
origin = Topology.Centroid(topology)
optimize = min(max(optimize, 0), 10)
if optimize > 0:
factor = (round(((11 - optimize)/30 + 0.57), 2))
flag = False
for n in range(10, 0, -1):
if flag:
break
za = n
zb = 90+n
zc = n
for z in range(za,zb,zc):
if flag:
break
t = Topology.Rotate(topology, origin=origin, axis=[0, 0, 1], angle=z)
minX, minY, maxX, maxY = br(t)
w = abs(maxX - minX)
l = abs(maxY - minY)
area = l*w
if area < orig_area*factor:
best_area = area
best_z = z
best_br = [minX, minY, maxX, maxY]
flag = True
break
if area < best_area:
best_area = area
best_z = z
best_br = [minX, minY, maxX, maxY]
else:
best_br = boundingRectangle
minX, minY, maxX, maxY = best_br
vb1 = Vertex.ByCoordinates(minX, minY, 0)
vb2 = Vertex.ByCoordinates(maxX, minY, 0)
vb3 = Vertex.ByCoordinates(maxX, maxY, 0)
vb4 = Vertex.ByCoordinates(minX, maxY, 0)
boundingRectangle = Wire.ByVertices([vb1, vb2, vb3, vb4], close=True)
boundingRectangle = Topology.Rotate(boundingRectangle, origin=origin, axis=[0, 0, 1], angle=-best_z)
boundingRectangle = Topology.Unflatten(boundingRectangle, origin=f_origin, direction=normal)
dictionary = Dictionary.ByKeysValues(["zrot"], [best_z])
boundingRectangle = Topology.SetDictionary(boundingRectangle, dictionary)
return boundingRectangle
@staticmethod
def ByEdges(edges: list, orient: bool = False, tolerance: float = 0.0001):
"""
Creates a wire from the input list of edges.
Parameters
----------
edges : list
The input list of edges.
orient : bool , optional
If set to True the edges are oriented head to tail. Otherwise, they are not. The default is False.
tolerance : float , optional
The desired tolerance. The default is 0.0001
Returns
-------
topologic_core.Wire
The created wire.
"""
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not isinstance(edges, list):
return None
edgeList = [x for x in edges if Topology.IsInstance(x, "Edge")]
if len(edgeList) == 0:
print("Wire.ByEdges - Error: The input edges list does not contain any valid edges. Returning None.")
return None
if len(edgeList) == 1:
wire = topologic.Wire.ByEdges(edgeList) # Hook to Core
else:
wire = Topology.SelfMerge(Cluster.ByTopologies(edgeList), tolerance=tolerance)
if not Topology.IsInstance(wire, "Wire"):
print("Wire.ByEdges - Error: The operation failed. Returning None.")
wire = None
if Wire.IsManifold(wire):
if orient == True:
wire = Wire.OrientEdges(wire, Wire.StartVertex(wire), tolerance=tolerance)
return wire
@staticmethod
def ByEdgesCluster(cluster, tolerance: float = 0.0001):
"""
Creates a wire from the input cluster of edges.
Parameters
----------
cluster : topologic_core.Cluster
The input cluster of edges.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created wire.
"""
if not Topology.IsInstance(cluster, "Cluster"):
print("Wire.ByEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
return None
edges = []
_ = cluster.Edges(None, edges)
return Wire.ByEdges(edges, tolerance=tolerance)
@staticmethod
def ByOffset(wire, offset: float = 1.0,
miter: bool = False, miterThreshold: float = None,
offsetKey: str = None, miterThresholdKey: str = None,
step: bool = True, angTolerance: float = 0.1, tolerance: float = 0.0001):
"""
Creates an offset wire from the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
offset : float , optional
The desired offset distance. The default is 1.0.
miter : bool , optional
if set to True, the corners will be mitered. The default is False.
miterThreshold : float , optional
The distance beyond which a miter should be added. The default is None which means the miter threshold is set to the offset distance multiplied by the square root of 2.
offsetKey : str , optional
If specified, the dictionary of the edges will be queried for this key to specify the desired offset. The default is None.
miterThresholdKey : str , optional
If specified, the dictionary of the vertices will be queried for this key to specify the desired miter threshold distance. The default is None.
step : bool , optional
If set to True, The transition between collinear edges with different offsets will be a step. Otherwise, it will be a continous edge. The default is True.
angTolerance : float , optional
The desired angular tolerance. The default is 0.1.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Dictionary import Dictionary
from random import randrange, sample
if not Topology.IsInstance(wire, "Wire"):
return None
if not miterThreshold:
miterThreshold = offset*math.sqrt(2)
flatFace = Face.ByWire(wire, tolerance=tolerance)
origin = Topology.Centroid(flatFace)
normal = Face.Normal(flatFace)
flatFace = Topology.Flatten(flatFace, origin=origin, direction=normal)
edges = Wire.Edges(wire)
vertices = Wire.Vertices(wire)
flatEdges = []
flatVertices = []
newEdges = []
for i in range(len(vertices)):
flatVertex = Topology.Flatten(vertices[i], origin=origin, direction=normal)
flatVertices.append(flatVertex)
vertices = flatVertices
for i in range(len(edges)):
flatEdge = Topology.Flatten(edges[i], origin=origin, direction=normal)
flatEdges.append(flatEdge)
if offsetKey:
d = Topology.Dictionary(edges[i])
value = Dictionary.ValueAtKey(d, key=offsetKey)
c = Topology.Centroid(flatEdge)
if value:
finalOffset = value
else:
finalOffset = offset
else:
finalOffset = offset
e1 = Edge.ByOffset2D(flatEdge,finalOffset)
newEdges.append(e1)
edges = flatEdges
newVertices = []
dupVertices = []
if Wire.IsClosed(wire):
e1 = newEdges[-1]
e2 = newEdges[0]
intV = Edge.Intersect2D(e1,e2)
if intV:
newVertices.append(intV)
dupVertices.append(vertices[0])
elif step:
edgeVertices= Edge.Vertices(e1)
newVertices.append(Vertex.NearestVertex(vertices[-1], Cluster.ByTopologies(edgeVertices), useKDTree=False))
edgeVertices= Edge.Vertices(e2)
newVertices.append(Vertex.NearestVertex(vertices[0], Cluster.ByTopologies(edgeVertices), useKDTree=False))
dupVertices.append(vertices[0])
dupVertices.append(vertices[0])
else:
tempEdge1 = Edge.ByVertices([Edge.StartVertex(e1), Edge.EndVertex(e2)], tolerance=tolerance, silent=True)
normal = Edge.Normal(e1)
normal = [normal[0]*finalOffset*10, normal[1]*finalOffset*10, normal[2]*finalOffset*10]
tempV = Vertex.ByCoordinates(vertices[0].X()+normal[0], vertices[0].Y()+normal[1], vertices[0].Z()+normal[2])
tempEdge2 = Edge.ByVertices([vertices[0], tempV], tolerance=tolerance, silent=True)
intV = Edge.Intersect2D(tempEdge1,tempEdge2)
newVertices.append(intV)
dupVertices.append(vertices[0])
else:
newVertices.append(Edge.StartVertex(newEdges[0]))
for i in range(len(newEdges)-1):
e1 = newEdges[i]
e2 = newEdges[i+1]
intV = Edge.Intersect2D(e1,e2)
if intV:
newVertices.append(intV)
dupVertices.append(vertices[i+1])
elif step:
newVertices.append(Edge.EndVertex(e1))
newVertices.append(Edge.StartVertex(e2))
dupVertices.append(vertices[i+1])
dupVertices.append(vertices[i+1])
else:
tempEdge1 = Edge.ByVertices([Edge.StartVertex(e1), Edge.EndVertex(e2)], tolerance=tolerance, silent=True)
normal = Edge.Normal(e1)
normal = [normal[0]*finalOffset*10, normal[1]*finalOffset*10, normal[2]*finalOffset*10]
tempV = Vertex.ByCoordinates(vertices[i+1].X()+normal[0], vertices[i+1].Y()+normal[1], vertices[i+1].Z()+normal[2])
tempEdge2 = Edge.ByVertices([vertices[i+1], tempV], tolerance=tolerance, silent=True)
intV = Edge.Intersect2D(tempEdge1,tempEdge2)
newVertices.append(intV)
dupVertices.append(vertices[i+1])
vertices = dupVertices
if not Wire.IsClosed(wire):
newVertices.append(Edge.EndVertex(newEdges[-1]))
newWire = Wire.ByVertices(newVertices, close=Wire.IsClosed(wire))
newVertices = Wire.Vertices(newWire)
newEdges = Wire.Edges(newWire)
miterEdges = []
cleanMiterEdges = []
# Handle miter
if miter:
for i in range(len(newVertices)):
if miterThresholdKey:
d = Topology.Dictionary(vertices[i])
value = Dictionary.ValueAtKey(d, key=miterThresholdKey)
if value:
finalMiterThreshold = value
else:
finalMiterThreshold = miterThreshold
else:
finalMiterThreshold = miterThreshold
if Vertex.Distance(vertices[i], newVertices[i]) > abs(finalMiterThreshold):
st = Topology.SuperTopologies(newVertices[i], newWire, topologyType="edge")
if len(st) > 1:
e1 = st[0]
e2 = st[1]
if not Edge.IsCollinear(e1, e2, tolerance=tolerance):
e1 = Edge.Reverse(e1, tolerance=tolerance)
bisector = Edge.ByVertices([vertices[i], newVertices[i]], tolerance=tolerance)
nv = Edge.VertexByDistance(bisector, distance=finalMiterThreshold, origin=Edge.StartVertex(bisector), tolerance=0.0001)
vec = Edge.Normal(bisector)
nv2 = Topology.Translate(nv, vec[0], vec[1], 0)
nv3 = Topology.Translate(nv, -vec[0], -vec[1], 0)
miterEdge = Edge.ByVertices([nv2,nv3], tolerance=tolerance)
if miterEdge:
miterEdge = Edge.SetLength(miterEdge, abs(offset)*10)
msv = Edge.Intersect2D(miterEdge, e1)
mev = Edge.Intersect2D(miterEdge, e2)
if (Vertex.IsInternal(msv, e1,tolerance=0.01) and (Vertex.IsInternal(mev, e2, tolerance=0.01))):
miterEdge = Edge.ByVertices([msv, mev], tolerance=tolerance)
if miterEdge:
cleanMiterEdges.append(miterEdge)
miterEdge = Edge.SetLength(miterEdge, Edge.Length(miterEdge)*1.02)
miterEdges.append(miterEdge)
c = Topology.SelfMerge(Cluster.ByTopologies(newEdges+miterEdges), tolerance=tolerance)
vertices = Wire.Vertices(c)
subtractEdges = []
for v in vertices:
edges = Topology.SuperTopologies(v, c, topologyType="edge")
if len(edges) == 2:
if not Edge.IsCollinear(edges[0], edges[1], tolerance=tolerance):
adjacentVertices = Topology.AdjacentTopologies(v, c)
total = 0
for adjV in adjacentVertices:
tempEdges = Topology.SuperTopologies(adjV, c, topologyType="edge")
total += len(tempEdges)
if total == 8:
subtractEdges = subtractEdges+edges
if len(subtractEdges) > 0:
newWire = Topology.Boolean(newWire, Cluster.ByTopologies(subtractEdges), operation="difference", tolerance=tolerance)
if len(cleanMiterEdges) > 0:
newWire = Topology.Boolean(newWire, Cluster.ByTopologies(cleanMiterEdges), operation="merge", tolerance=tolerance)
newWire = Topology.Unflatten(newWire, origin=origin, direction=normal)
return newWire
@staticmethod
def ByVertices(vertices: list, close: bool = True, tolerance: float = 0.0001):
"""
Creates a wire from the input list of vertices.
Parameters
----------
vertices : list
the input list of vertices.
close : bool , optional
If True the last vertex will be connected to the first vertex to close the wire. The default is True.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created wire.
"""
from topologicpy.Edge import Edge
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not isinstance(vertices, list):
return None
vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
if len(vertexList) < 2:
print("Wire.ByVertices - Error: The number of vertices is less than 2. Returning None.")
return None
edges = []
for i in range(len(vertexList)-1):
v1 = vertexList[i]
v2 = vertexList[i+1]
e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True)
if Topology.IsInstance(e, "Edge"):
edges.append(e)
if close:
v1 = vertexList[-1]
v2 = vertexList[0]
e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True)
if Topology.IsInstance(e, "Edge"):
edges.append(e)
if len(edges) < 1:
print("Wire.ByVertices - Error: The number of edges is less than 1. Returning None.")
return None
elif len(edges) == 1:
wire = Wire.ByEdges(edges, orient=False)
else:
wire = Topology.SelfMerge(Cluster.ByTopologies(edges), tolerance=tolerance)
return wire
@staticmethod
def ByVerticesCluster(cluster, close: bool = True):
"""
Creates a wire from the input cluster of vertices.
Parameters
----------
cluster : topologic_core.cluster
the input cluster of vertices.
close : bool , optional
If True the last vertex will be connected to the first vertex to close the wire. The default is True.
Returns
-------
topologic_core.Wire
The created wire.
"""
if not Topology.IsInstance(cluster, "Cluster"):
return None
vertices = []
_ = cluster.Vertices(None, vertices)
return Wire.ByVertices(vertices, close)
@staticmethod
def Circle(origin= None, radius: float = 0.5, sides: int = 16, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates a circle.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
radius : float , optional
The radius of the circle. The default is 0.5.
sides : int , optional
The number of sides of the circle. The default is 16.
fromAngle : float , optional
The angle in degrees from which to start creating the arc of the circle. The default is 0.
toAngle : float , optional
The angle in degrees at which to end creating the arc of the circle. The default is 360.
close : bool , optional
If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open.
direction : list , optional
The vector representing the up direction of the circle. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created circle.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.Circle - Error: The input origin parameter is not a valid Vertex. Retruning None.")
return None
if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]:
print("Wire.Circle - Error: The input placement parameter is not a recognised string. Retruning None.")
return None
radius = abs(radius)
if radius < tolerance:
return None
if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance:
return None
baseV = []
xList = []
yList = []
if toAngle < fromAngle:
toAngle += 360
if abs(toAngle-fromAngle) < tolerance:
return None
angleRange = toAngle - fromAngle
fromAngle = math.radians(fromAngle)
toAngle = math.radians(toAngle)
sides = int(math.floor(sides))
for i in range(sides+1):
angle = fromAngle + math.radians(angleRange/sides)*i
x = math.cos(angle)*radius + origin.X()
y = math.sin(angle)*radius + origin.Y()
z = origin.Z()
xList.append(x)
yList.append(y)
baseV.append(Vertex.ByCoordinates(x, y, z))
if angleRange == 360:
baseWire = Wire.ByVertices(baseV[::-1], close=False) #reversing the list so that the normal points up in Blender
else:
baseWire = Wire.ByVertices(baseV[::-1], close=close) #reversing the list so that the normal points up in Blender
if placement.lower() == "lowerleft":
baseWire = Topology.Translate(baseWire, radius, radius, 0)
elif placement.lower() == "upperleft":
baseWire = Topology.Translate(baseWire, radius, -radius, 0)
elif placement.lower() == "lowerright":
baseWire = Topology.Translate(baseWire, -radius, radius, 0)
elif placement.lower() == "upperright":
baseWire = Topology.Translate(baseWire, -radius, -radius, 0)
if direction != [0, 0, 1]:
baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction)
return baseWire
@staticmethod
def Close(wire, mantissa=6, tolerance=0.0001):
"""
Closes the input wire
Parameters
----------
wire : topologic_core.Wire
The input wire.
mantissa : int , optional
The desired length of the mantissa. The default is 6.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The closed version of the input wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Helper import Helper
def nearest_vertex(vertex, vertices):
distances = []
for v in vertices:
distances.append(Vertex.Distance(vertex, v))
new_vertices = Helper.Sort(vertices, distances)
return new_vertices[1] #The first item is the same vertex, so return the next nearest vertex.
if not Topology.IsInstance(wire, "Wire"):
print("Wire.Close - Error: The input wire parameter is not a valid topologic wire. Returning None.")
return None
if Wire.IsClosed(wire):
return wire
vertices = Topology.Vertices(wire)
ends = [v for v in vertices if Vertex.Degree(v, wire) == 1]
if len(ends) < 2:
print("Wire.Close - Error: The input wire parameter contains less than two open end vertices. Returning None.")
return None
geometry = Topology.Geometry(wire, mantissa=mantissa)
g_vertices = geometry['vertices']
g_edges = geometry['edges']
used = []
for end in ends:
nearest = nearest_vertex(end, ends)
if not nearest in used:
d = Vertex.Distance(end, nearest)
i1 = Vertex.Index(end, vertices)
i2 = Vertex.Index(nearest, vertices)
if i1 == None or i2 == None:
print("Wire.Close - Error: Something went wrong. Returning None.")
return None
if d < tolerance:
g_vertices[i1] = Vertex.Coordinates(end)
g_vertices[i2] = Vertex.Coordinates(end)
else:
if not(([i1, i2] in g_edges) or ([i2, i1] in g_edges)):
g_edges.append([i1, i2])
used.append(end)
new_wire = Topology.ByGeometry(vertices=g_vertices, edges=g_edges, faces=[], outputMode="wire")
return new_wire
@staticmethod
def ConvexHull(topology, tolerance: float = 0.0001):
"""
Returns a wire representing the 2D convex hull of the input topology. The vertices of the topology are assumed to be coplanar.
Parameters
----------
topology : topologic_core.Topology
The input topology.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The convex hull of the input topology.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Face import Face
from topologicpy.Topology import Topology
from topologicpy.Dictionary import Dictionary
from random import sample
def Left_index(points):
'''
Finding the left most point
'''
minn = 0
for i in range(1,len(points)):
if points[i][0] < points[minn][0]:
minn = i
elif points[i][0] == points[minn][0]:
if points[i][1] > points[minn][1]:
minn = i
return minn
def orientation(p, q, r):
'''
To find orientation of ordered triplet (p, q, r).
The function returns following values
0 --> p, q and r are collinear
1 --> Clockwise
2 --> Counterclockwise
'''
val = (q[1] - p[1]) * (r[0] - q[0]) - \
(q[0] - p[0]) * (r[1] - q[1])
if val == 0:
return 0
elif val > 0:
return 1
else:
return 2
def convex_hull(points, n):
# There must be at least 3 points
if n < 3:
return
# Find the leftmost point
l = Left_index(points)
hull = []
'''
Start from leftmost point, keep moving counterclockwise
until reach the start point again. This loop runs O(h)
times where h is number of points in result or output.
'''
p = l
q = 0
while(True):
# Add current point to result
hull.append(p)
'''
Search for a point 'q' such that orientation(p, q,
x) is counterclockwise for all points 'x'. The idea
is to keep track of last visited most counterclock-
wise point in q. If any point 'i' is more counterclock-
wise than q, then update q.
'''
q = (p + 1) % n
for i in range(n):
# If i is more counterclockwise
# than current q, then update q
if(orientation(points[p],
points[i], points[q]) == 2):
q = i
'''
Now q is the most counterclockwise with respect to p
Set p as q for next iteration, so that q is added to
result 'hull'
'''
p = q
# While we don't come to first point
if(p == l):
break
# Print Result
return hull
f = None
# Create a sample face and flatten
while not Topology.IsInstance(f, "Face"):
vertices = Topology.SubTopologies(topology=topology, subTopologyType="vertex")
v = sample(vertices, 3)
w = Wire.ByVertices(v)
f = Face.ByWire(w, tolerance=tolerance)
origin = Topology.Centroid(f)
normal = Face.Normal(f)
f = Topology.Flatten(f, origin=origin, direction=normal)
topology = Topology.Flatten(topology, origin=origin, direction=normal)
vertices = Topology.Vertices(topology)
points = []
for v in vertices:
points.append((Vertex.X(v), Vertex.Y(v)))
hull = convex_hull(points, len(points))
hull_vertices = []
for p in hull:
hull_vertices.append(Vertex.ByCoordinates(points[p][0], points[p][1], 0))
ch = Wire.ByVertices(hull_vertices)
ch = Topology.Unflatten(ch, origin=origin, direction=normal)
return ch
@staticmethod
def Cycles(wire, maxVertices: int = 4, tolerance: float = 0.0001) -> list:
"""
Returns the closed circuits of wires found within the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
maxVertices : int , optional
The maximum number of vertices of the circuits to be searched. The default is 4.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
list
The list of circuits (closed wires) found within the input wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
def vIndex(v, vList, tolerance):
for i in range(len(vList)):
if Vertex.Distance(v, vList[i]) < tolerance:
return i+1
return None
# rotate cycle path such that it begins with the smallest node
def rotate_to_smallest(path):
n = path.index(min(path))
return path[n:]+path[:n]
def invert(path):
return rotate_to_smallest(path[::-1])
def isNew(cycles, path):
return not path in cycles
def visited(node, path):
return node in path
def findNewCycles(graph, cycles, path, maxVertices):
if len(path) > maxVertices:
return
start_node = path[0]
next_node= None
sub = []
#visit each edge and each node of each edge
for edge in graph:
node1, node2 = edge
if start_node in edge:
if node1 == start_node:
next_node = node2
else:
next_node = node1
if not visited(next_node, path):
# neighbor node not on path yet
sub = [next_node]
sub.extend(path)
# explore extended path
findNewCycles(graph, cycles, sub, maxVertices);
elif len(path) > 2 and next_node == path[-1]:
# cycle found
p = rotate_to_smallest(path);
inv = invert(p)
if isNew(cycles, p) and isNew(cycles, inv):
cycles.append(p)
def main(graph, cycles, maxVertices):
returnValue = []
for edge in graph:
for node in edge:
findNewCycles(graph, cycles, [node], maxVertices)
for cy in cycles:
row = []
for node in cy:
row.append(node)
returnValue.append(row)
return returnValue
tEdges = []
_ = wire.Edges(None, tEdges)
tVertices = []
_ = wire.Vertices(None, tVertices)
tVertices = tVertices
graph = []
for anEdge in tEdges:
graph.append([vIndex(anEdge.StartVertex(), tVertices, tolerance), vIndex(anEdge.EndVertex(), tVertices, tolerance)])
cycles = []
resultingCycles = main(graph, cycles, maxVertices)
result = []
for aRow in resultingCycles:
row = []
for anIndex in aRow:
row.append(tVertices[anIndex-1])
result.append(row)
resultWires = []
for i in range(len(result)):
c = result[i]
resultEdges = []
for j in range(len(c)-1):
v1 = c[j]
v2 = c[j+1]
e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True)
resultEdges.append(e)
e = Edge.ByStartVertexEndVertex(c[len(c)-1], c[0], tolerance=tolerance, silent=True)
resultEdges.append(e)
resultWire = Wire.ByEdges(resultEdges, tolerance=tolerance)
resultWires.append(resultWire)
return resultWires
@staticmethod
def Edges(wire) -> list:
"""
Returns the edges of the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
Returns
-------
list
The list of edges.
"""
if not Topology.IsInstance(wire, "Wire"):
return None
edges = []
_ = wire.Edges(None, edges)
return edges
@staticmethod
def Einstein(origin= None, radius: float = 0.5, direction: list = [0, 0, 1], placement: str = "center"):
"""
Creates an aperiodic monotile, also called an 'einstein' tile (meaning one tile in German, not the name of the famous physist). See https://arxiv.org/abs/2303.10798
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the tile. The default is None which results in the tiles first vertex being placed at (0, 0, 0).
radius : float , optional
The radius of the hexagon determining the size of the tile. The default is 0.5.
direction : list , optional
The vector representing the up direction of the ellipse. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the hexagon determining the location of the tile. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
"""
from topologicpy.Vertex import Vertex
from topologicpy.Topology import Topology
import math
def cos(angle):
return math.cos(math.radians(angle))
def sin(angle):
return math.sin(math.radians(angle))
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
d = cos(30)*radius
v1 = Vertex.ByCoordinates(0, 0, 0)
v2 = Vertex.ByCoordinates(cos(30)*d, sin(30)*d, 0)
v3 = Vertex.ByCoordinates(radius, 0)
v4 = Vertex.ByCoordinates(2*radius, 0)
v5 = Vertex.ByCoordinates(2*radius+cos(60)*radius*0.5, sin(30)*d, 0)
v6 = Vertex.ByCoordinates(1.5*radius, d)
v7 = Vertex.ByCoordinates(1.5*radius, 2*d)
v8 = Vertex.ByCoordinates(radius, 2*d)
v9 = Vertex.ByCoordinates(radius-cos(60)*0.5*radius, 2*d+sin(60)*0.5*radius)
v10 = Vertex.ByCoordinates(0, 2*d)
v11 = Vertex.ByCoordinates(0, d)
v12 = Vertex.ByCoordinates(-radius*0.5, d)
v13 = Vertex.ByCoordinates(-cos(30)*d, sin(30)*d, 0)
einstein = Wire.ByVertices([v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13], close=True)
if placement.lower() == "lowerleft":
einstein = Topology.Translate(einstein, radius, d, 0)
dx = Vertex.X(origin)
dy = Vertex.Y(origin)
dz = Vertex.Z(origin)
einstein = Topology.Translate(einstein, dx, dy, dz)
if direction != [0, 0, 1]:
einstein = Topology.Orient(einstein, origin=origin, dirA=[0, 0, 1], dirB=direction)
return einstein
@staticmethod
def Ellipse(origin= None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: float = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates an ellipse and returns all its geometry and parameters.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0).
inputMode : int , optional
The method by wich the ellipse is defined. The default is 1.
Based on the inputMode value, only the following inputs will be considered. The options are:
1. Width and Length (considered inputs: width, length)
2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity)
3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength)
4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength)
width : float , optional
The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1.
length : float , optional
The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1.
focalLength : float , optional
The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3.
eccentricity : float , optional
The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2.
majorAxisLength : float , optional
The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4.
minorAxisLength : float , optional
The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4.
sides : int , optional
The number of sides of the ellipse. The default is 32.
fromAngle : float , optional
The angle in degrees from which to start creating the arc of the ellipse. The default is 0.
toAngle : float , optional
The angle in degrees at which to end creating the arc of the ellipse. The default is 360.
close : bool , optional
If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open.
direction : list , optional
The vector representing the up direction of the ellipse. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created ellipse
"""
ellipseAll = Wire.EllipseAll(origin=origin, inputMode=inputMode, width=width, length=length, focalLength=focalLength, eccentricity=eccentricity, majorAxisLength=majorAxisLength, minorAxisLength=minorAxisLength, sides=sides, fromAngle=fromAngle, toAngle=toAngle, close=close, direction=direction, placement=placement, tolerance=tolerance)
return ellipseAll["ellipse"]
@staticmethod
def EllipseAll(origin= None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str ="center", tolerance: float = 0.0001):
"""
Creates an ellipse and returns all its geometry and parameters.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0).
inputMode : int , optional
The method by wich the ellipse is defined. The default is 1.
Based on the inputMode value, only the following inputs will be considered. The options are:
1. Width and Length (considered inputs: width, length)
2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity)
3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength)
4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength)
width : float , optional
The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1.
length : float , optional
The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1.
focalLength : float , optional
The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3.
eccentricity : float , optional
The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2.
majorAxisLength : float , optional
The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4.
minorAxisLength : float , optional
The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4.
sides : int , optional
The number of sides of the ellipse. The default is 32.
fromAngle : float , optional
The angle in degrees from which to start creating the arc of the ellipse. The default is 0.
toAngle : float , optional
The angle in degrees at which to end creating the arc of the ellipse. The default is 360.
close : bool , optional
If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open.
direction : list , optional
The vector representing the up direction of the ellipse. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
dictionary
A dictionary with the following keys and values:
1. "ellipse" : The ellipse (topologic_core.Wire)
2. "foci" : The two focal points (topologic_core.Cluster containing two vertices)
3. "a" : The major axis length
4. "b" : The minor axis length
5. "c" : The focal length
6. "e" : The eccentricity
7. "width" : The width
8. "length" : The length
"""
from topologicpy.Vertex import Vertex
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
return None
if inputMode not in [1, 2, 3, 4]:
return None
if placement.lower() not in ["center", "lowerleft"]:
return None
if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance:
return None
width = abs(width)
length = abs(length)
focalLength= abs(focalLength)
eccentricity=abs(eccentricity)
majorAxisLength=abs(majorAxisLength)
minorAxisLength=abs(minorAxisLength)
sides = abs(sides)
if width < tolerance or length < tolerance or focalLength < tolerance or eccentricity < tolerance or majorAxisLength < tolerance or minorAxisLength < tolerance or sides < 3:
return None
if inputMode == 1:
w = width
l = length
a = width/2
b = length/2
c = math.sqrt(abs(b**2 - a**2))
e = c/a
elif inputMode == 2:
c = focalLength
e = eccentricity
a = c/e
b = math.sqrt(abs(a**2 - c**2))
w = a*2
l = b*2
elif inputMode == 3:
c = focalLength
b = minorAxisLength
a = math.sqrt(abs(b**2 + c**2))
e = c/a
w = a*2
l = b*2
elif inputMode == 4:
a = majorAxisLength
b = minorAxisLength
c = math.sqrt(abs(b**2 - a**2))
e = c/a
w = a*2
l = b*2
else:
return None
baseV = []
xList = []
yList = []
if toAngle < fromAngle:
toAngle += 360
if abs(toAngle - fromAngle) < tolerance:
return None
angleRange = toAngle - fromAngle
fromAngle = math.radians(fromAngle)
toAngle = math.radians(toAngle)
sides = int(math.floor(sides))
for i in range(sides+1):
angle = fromAngle + math.radians(angleRange/sides)*i
x = math.sin(angle)*a + origin.X()
y = math.cos(angle)*b + origin.Y()
z = origin.Z()
xList.append(x)
yList.append(y)
baseV.append(Vertex.ByCoordinates(x, y, z))
if angleRange == 360:
baseWire = Wire.ByVertices(baseV[::-1], close=False) #reversing the list so that the normal points up in Blender
else:
baseWire = Wire.ByVertices(baseV[::-1], close=close) #reversing the list so that the normal points up in Blender
if placement.lower() == "lowerleft":
baseWire = Topology.Translate(baseWire, a, b, 0)
baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction)
# Create a Cluster of the two foci
v1 = Vertex.ByCoordinates(c+origin.X(), 0+origin.Y(), 0)
v2 = Vertex.ByCoordinates(-c+origin.X(), 0+origin.Y(), 0)
foci = Cluster.ByTopologies([v1, v2])
if placement.lower() == "lowerleft":
foci = Topology.Translate(foci, a, b, 0)
foci = Topology.Orient(foci, origin=origin, dirA=[0, 0, 1], dirB=direction)
d = {}
d['ellipse'] = baseWire
d['foci'] = foci
d['a'] = a
d['b'] = b
d['c'] = c
d['e'] = e
d['w'] = w
d['l'] = l
return d
@staticmethod
def EndVertex(wire):
"""
Returns the end vertex of the input wire. The wire must be manifold and open.
"""
sv, ev = Wire.StartEndVertices(wire)
return ev
@staticmethod
def ExteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) -> list:
"""
Returns the exterior angles of the input wire in degrees. The wire must be planar, manifold, and closed.
Parameters
----------
wire : topologic_core.Wire
The input wire.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
mantissa : int , optional
The length of the desired mantissa. The default is 6.
Returns
-------
list
The list of exterior angles.
"""
if not Topology.IsInstance(wire, "Wire"):
print("Wire.InteriorAngles - Error: The input wire parameter is not a valid wire. Returning None")
return None
if not Wire.IsManifold(wire):
print("Wire.InteriorAngles - Error: The input wire parameter is non-manifold. Returning None")
return None
if not Wire.IsClosed(wire):
print("Wire.InteriorAngles - Error: The input wire parameter is not closed. Returning None")
return None
interior_angles = Wire.InteriorAngles(wire, mantissa=mantissa)
exterior_angles = [round(360-a, mantissa) for a in interior_angles]
return exterior_angles
@staticmethod
def Fillet(wire, radius: float = 0, radiusKey: str = None, tolerance: float = 0.0001, silent: bool = False):
"""
Fillets (rounds) the interior and exterior corners of the input wire given the input radius. See https://en.wikipedia.org/wiki/Fillet_(mechanics)
Parameters
----------
wire : topologic_core.Wire
The input wire.
radius : float
The desired radius of the fillet.
radiusKey : str , optional
If specified, the dictionary of the vertices will be queried for this key to specify the desired fillet radius. The default is None.
tolerance : float , optional
The desired tolerance. 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
-------
topologic_core.Wire
The filleted wire.
"""
def start_from(edge, v):
sv = Edge.StartVertex(edge)
ev = Edge.EndVertex(edge)
if Vertex.Distance(v, ev) < Vertex.Distance(v, sv):
return Edge.Reverse(edge)
return edge
def compute_kite_edges(alpha, r):
# Convert angle to radians
alpha = math.radians(alpha) *0.5
h = r/math.cos(alpha)
a = math.sqrt(h*h - r*r)
return [a,h]
import math
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Vector import Vector
from topologicpy.Dictionary import Dictionary
if not Topology.IsInstance(wire, "Wire"):
if not silent:
print("Wire.Fillet - Error: The input wire parameter is not a valid wire. Returning None.")
return None
if not Wire.IsManifold(wire):
if not silent:
print("Wire.Fillet - Error: The input wire parameter is not manifold. Returning None.")
return None
if not Topology.IsPlanar(wire):
if not silent:
print("Wire.Fillet - Error: The input wire parameter is not planar. Returning None.")
return None
orig_radius = radius
f = Face.BoundingRectangle(wire, tolerance=tolerance)
normal = Face.Normal(f)
flat_wire = Topology.Flatten(wire, origin=Vertex.Origin(), direction=normal)
vertices = Topology.Vertices(flat_wire)
final_vertices = []
fillets = []
for v in vertices:
radius = orig_radius
edges = Topology.SuperTopologies(v, flat_wire, topologyType="edge")
if len(edges) == 2:
for edge in edges:
ev = Edge.EndVertex(edge)
if Vertex.Distance(v, ev) < tolerance:
edge0 = edge
else:
edge1 = edge
ang = Edge.Angle(edge0, edge1)
e1 = start_from(edge0, v)
e2 = start_from(edge1, v)
dir1 = Edge.Direction(e1)
dir2 = Edge.Direction(e2)
if Vector.IsParallel(dir1, dir2) or Vector.IsAntiParallel(dir1, dir2):
pass
else:
if isinstance(radiusKey, str):
d = Topology.Dictionary(v)
if Topology.IsInstance(d, "Dictionary"):
v_radius = Dictionary.ValueAtKey(d, radiusKey)
if isinstance(v_radius, float) or isinstance(v_radius, int):
if v_radius >= 0:
radius = v_radius
if radius > 0:
dir_bisector = Vector.Bisect(dir1,dir2)
a, h = compute_kite_edges(ang, radius)
if a <= Edge.Length(e1) and a <= Edge.Length(e2):
v1 = Topology.TranslateByDirectionDistance(v, dir1, a)
center = Topology.TranslateByDirectionDistance(v, dir_bisector, h)
v2 = Topology.TranslateByDirectionDistance(v, dir2, a)
r1 = Edge.ByVertices(center, v1)
dir1 = Edge.Direction(r1)
r2 = Edge.ByVertices(center, v2)
dir2 = Edge.Direction(r2)
compass1 = Vector.CompassAngle(Vector.East(), dir1)*-1
compass2 = Vector.CompassAngle(Vector.East(), dir2)*-1
if compass2 < compass1:
temp = compass2
compass2 = compass1
compass1 = temp
w1 = Wire.Circle(origin=center, radius=radius, fromAngle=compass1, toAngle=compass2, close=False)
w2 = Wire.Circle(origin=center, radius=radius, fromAngle=compass2, toAngle=compass1, close=False)
if Wire.Length(w1) < Wire.Length(w2):
fillet = w1
else:
fillet = w2
f_sv = Wire.StartVertex(fillet)
if Vertex.Distance(f_sv, edge1) < Vertex.Distance(f_sv, edge0):
fillet = Wire.Reverse(fillet)
final_vertices += Topology.Vertices(fillet)
else:
if not silent:
print("Wire.Fillet - Error: The specified fillet radius is too large to be applied. Skipping.")
else:
final_vertices.append(v)
else:
final_vertices.append(v)
flat_wire = Wire.ByVertices(final_vertices, close=Wire.IsClosed(wire))
# Unflatten the wire
return_wire = Topology.Unflatten(flat_wire, origin=Vertex.Origin(), direction=normal)
return return_wire
@staticmethod
def InteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) -> list:
"""
Returns the interior angles of the input wire in degrees. The wire must be planar, manifold, and closed.
Parameters
----------
wire : topologic_core.Wire
The input wire.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
mantissa : int , optional
The desired length of the mantissa. The default is 6.
Returns
-------
list
The list of interior angles.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Face import Face
from topologicpy.Topology import Topology
from topologicpy.Vector import Vector
from topologicpy.Dictionary import Dictionary
if not Topology.IsInstance(wire, "Wire"):
print("Wire.InteriorAngles - Error: The input wire parameter is not a valid wire. Returning None")
return None
if not Wire.IsManifold(wire):
print("Wire.InteriorAngles - Error: The input wire parameter is non-manifold. Returning None")
return None
if not Wire.IsClosed(wire):
print("Wire.InteriorAngles - Error: The input wire parameter is not closed. Returning None")
return None
f = Face.ByWire(wire)
normal = Face.Normal(f)
origin = Topology.Centroid(f)
w = Topology.Flatten(wire, origin=origin, direction=normal)
angles = []
edges = Topology.Edges(w)
for i in range(len(edges)-1):
e1 = edges[i]
e2 = edges[i+1]
a = round(360 - Vector.CompassAngle(Edge.Direction(e1), Edge.Direction(e2)), mantissa)
angles.append(a)
e1 = edges[len(edges)-1]
e2 = edges[0]
a = round(360 - Vector.CompassAngle(Edge.Direction(e1), Edge.Direction(e2)), mantissa)
angles = [a]+angles
return angles
@staticmethod
def Interpolate(wires: list, n: int = 5, outputType: str = "default", mapping: str = "default", tolerance: float = 0.0001):
"""
Creates *n* number of wires that interpolate between wireA and wireB.
Parameters
----------
wireA : topologic_core.Wire
The first input wire.
wireB : topologic_core.Wire
The second input wire.
n : int , optional
The number of intermediate wires to create. The default is 5.
outputType : str , optional
The desired type of output. The options are case insensitive. The default is "contour". The options are:
- "Default" or "Contours" (wires are not connected)
- "Raster or "Zigzag" or "Toolpath" (the wire ends are connected to create a continous path)
- "Grid" (the wire ends are connected to create a grid).
mapping : str , optional
The desired type of mapping for wires with different number of vertices. It is case insensitive. The default is "default". The options are:
- "Default" or "Repeat" which repeats the last vertex of the wire with the least number of vertices
- "Nearest" which maps the vertices of one wire to the nearest vertex of the next wire creating a list of equal number of vertices.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
tTopology
The created interpolated wires as well as the input wires. The return type can be a topologic_core.Cluster or a topologic_core.Wire based on options.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Helper import Helper
outputType = outputType.lower()
if outputType not in ["default", "contours", "raster", "zigzag", "toolpath", "grid"]:
return None
if outputType == "default" or outputType == "contours":
outputType = "contours"
if outputType == "raster" or outputType == "zigzag" or outputType == "toolpath":
outputType = "zigzag"
mapping = mapping.lower()
if mapping not in ["default", "nearest", "repeat"]:
print("Wire.Interpolate - Error: The mapping input parameter is not recognized. Returning None.")
return None
def nearestVertex(v, vertices):
distances = [Vertex.Distance(v, vertex) for vertex in vertices]
return vertices[distances.index(sorted(distances)[0])]
def replicate(vertices, mapping="default"):
vertices = Helper.Repeat(vertices)
finalList = vertices
if mapping == "nearest":
finalList = [vertices[0]]
for i in range(len(vertices)-1):
loopA = vertices[i]
loopB = vertices[i+1]
nearestVertices = []
for j in range(len(loopA)):
nv = nearestVertex(loopA[j], loopB)
nearestVertices.append(nv)
finalList.append(nearestVertices)
return finalList
def process(verticesA, verticesB, n=5):
contours = [verticesA]
for i in range(1, n+1):
u = float(i)/float(n+1)
temp_vertices = []
for j in range(len(verticesA)):
temp_v = Edge.VertexByParameter(Edge.ByVertices([verticesA[j], verticesB[j]], tolerance=tolerance), u)
temp_vertices.append(temp_v)
contours.append(temp_vertices)
return contours
if len(wires) < 2:
return None
vertices = []
for wire in wires:
vertices.append(Topology.SubTopologies(wire, subTopologyType="vertex"))
vertices = replicate(vertices, mapping=mapping)
contours = []
finalWires = []
for i in range(len(vertices)-1):
verticesA = vertices[i]
verticesB = vertices[i+1]
contour = process(verticesA=verticesA, verticesB=verticesB, n=n)
contours += contour
for c in contour:
finalWires.append(Wire.ByVertices(c, Wire.IsClosed(wires[i])))
contours.append(vertices[-1])
finalWires.append(wires[-1])
ridges = []
if outputType == "grid" or outputType == "zigzag":
for i in range(len(contours)-1):
verticesA = contours[i]
verticesB = contours[i+1]
if outputType == "grid":
for j in range(len(verticesA)):
ridges.append(Edge.ByVertices([verticesA[j], verticesB[j]], tolerance=tolerance))
elif outputType == "zigzag":
if i%2 == 0:
sv = verticesA[-1]
ev = verticesB[-1]
ridges.append(Edge.ByVertices([sv, ev], tolerance=tolerance))
else:
sv = verticesA[0]
ev = verticesB[0]
ridges.append(Edge.ByVertices([sv, ev], tolerance=tolerance))
return Topology.SelfMerge(Cluster.ByTopologies(finalWires+ridges), tolerance=tolerance)
@staticmethod
def Invert(wire):
"""
Creates a wire that is an inverse (mirror) of the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
Returns
-------
topologic_core.Wire
The inverted wire.
"""
if not Topology.IsInstance(wire, "Wire"):
return None
vertices = Wire.Vertices(wire)
reversed_vertices = vertices[::-1]
return Wire.ByVertices(reversed_vertices)
@staticmethod
def IsClosed(wire) -> bool:
"""
Returns True if the input wire is closed. Returns False otherwise.
Parameters
----------
wire : topologic_core.Wire
The input wire.
Returns
-------
bool
True if the input wire is closed. False otherwise.
"""
status = None
if wire:
if Topology.IsInstance(wire, "Wire"):
status = wire.IsClosed()
return status
@staticmethod
def IsManifold(wire) -> bool:
"""
Returns True if the input wire is manifold. Returns False otherwise. A manifold wire is one where its vertices have a degree of 1 or 2.
Parameters
----------
wire : topologic_core.Wire
The input wire.
Returns
-------
bool
True if the input wire is manifold. False otherwise.
"""
from topologicpy.Vertex import Vertex
if not Topology.IsInstance(wire, "Wire"):
print("Wire.IsManifold - Error: The input wire parameter is not a valid topologic wire. Returning None.")
return None
vertices = Wire.Vertices(wire)
for v in vertices:
if Vertex.Degree(v, hostTopology=wire) > 2:
return False
return True
@staticmethod
def IsSimilar(wireA, wireB, angTolerance: float = 0.1, tolerance: float = 0.0001) -> bool:
"""
Returns True if the input wires are similar. Returns False otherwise. The wires must be closed.
Parameters
----------
wireA : topologic_core.Wire
The first input wire.
wireB : topologic_core.Wire
The second input wire.
angTolerance : float , optional
The desired angular tolerance. The default is 0.1.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
bool
True if the two input wires are similar. False otherwise.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
def isCyclicallyEquivalent(u, v, lengthTolerance, angleTolerance):
n, i, j = len(u), 0, 0
if n != len(v):
return False
while i < n and j < n:
if (i % 2) == 0:
tol = lengthTolerance
else:
tol = angleTolerance
k = 1
while k <= n and math.fabs(u[(i + k) % n]- v[(j + k) % n]) <= tol:
k += 1
if k > n:
return True
if math.fabs(u[(i + k) % n]- v[(j + k) % n]) > tol:
i += k
else:
j += k
return False
def angleBetweenEdges(e1, e2, tolerance):
a = e1.EndVertex().X() - e1.StartVertex().X()
b = e1.EndVertex().Y() - e1.StartVertex().Y()
c = e1.EndVertex().Z() - e1.StartVertex().Z()
d = Vertex.Distance(e1.EndVertex(), e2.StartVertex())
if d <= tolerance:
d = e2.StartVertex().X() - e2.EndVertex().X()
e = e2.StartVertex().Y() - e2.EndVertex().Y()
f = e2.StartVertex().Z() - e2.EndVertex().Z()
else:
d = e2.EndVertex().X() - e2.StartVertex().X()
e = e2.EndVertex().Y() - e2.StartVertex().Y()
f = e2.EndVertex().Z() - e2.StartVertex().Z()
dotProduct = a*d + b*e + c*f
modOfVector1 = math.sqrt( a*a + b*b + c*c)*math.sqrt(d*d + e*e + f*f)
angle = dotProduct/modOfVector1
angleInDegrees = math.degrees(math.acos(angle))
return angleInDegrees
def getInteriorAngles(edges, tolerance):
angles = []
for i in range(len(edges)-1):
e1 = edges[i]
e2 = edges[i+1]
angles.append(angleBetweenEdges(e1, e2, tolerance))
return angles
def getRep(edges, tolerance):
angles = getInteriorAngles(edges, tolerance)
lengths = []
for anEdge in edges:
lengths.append(Edge.Length(anEdge))
minLength = min(lengths)
normalisedLengths = []
for aLength in lengths:
normalisedLengths.append(aLength/minLength)
return [x for x in itertools.chain(*itertools.zip_longest(normalisedLengths, angles)) if x is not None]
if (wireA.IsClosed() == False):
return None
if (wireB.IsClosed() == False):
return None
edgesA = []
_ = wireA.Edges(None, edgesA)
edgesB = []
_ = wireB.Edges(None, edgesB)
if len(edgesA) != len(edgesB):
return False
repA = getRep(list(edgesA), tolerance)
repB = getRep(list(edgesB), tolerance)
if isCyclicallyEquivalent(repA, repB, tolerance, angTolerance):
return True
if isCyclicallyEquivalent(repA, repB[::-1], tolerance, angTolerance):
return True
return False
@staticmethod
def Length(wire, mantissa: int = 6) -> float:
"""
Returns the length of the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
mantissa : int , optional
The desired length of the mantissa. The default is 6.
Returns
-------
float
The length of the input wire. Test
"""
from topologicpy.Edge import Edge
if not wire:
return None
if not Topology.IsInstance(wire, "Wire"):
return None
totalLength = None
try:
edges = []
_ = wire.Edges(None, edges)
totalLength = 0
for anEdge in edges:
totalLength = totalLength + Edge.Length(anEdge)
totalLength = round(totalLength, mantissa)
except:
totalLength = None
return totalLength
@staticmethod
def Line(origin= None, length: float = 1, direction: list = [1, 0, 0], sides: int = 2, placement: str ="center"):
"""
Creates a straight line wire using the input parameters.
Parameters
----------
origin : topologic_core.Vertex , optional
The origin location of the box. The default is None which results in the edge being placed at (0, 0, 0).
length : float , optional
The desired length of the edge. The default is 1.0.
direction : list , optional
The desired direction (vector) of the edge. The default is [1, 0, 0] (along the X-axis).
sides : int , optional
The desired number of sides/segments. The minimum number of sides is 2. The default is 2.
placement : str , optional
The desired placement of the edge. The options are:
1. "center" which places the center of the edge at the origin.
2. "start" which places the start of the edge at the origin.
3. "end" which places the end of the edge at the origin.
The default is "center".
Returns
-------
topologic_core.Edge
The created edge
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Vector import Vector
from topologicpy.Topology import Topology
if origin == None:
origin = Vertex.Origin()
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.Line - Error: The input origin is not a valid vertex. Returning None.")
return None
if length <= 0:
print("Wire.Line - Error: The input length is less than or equal to zero. Returning None.")
return None
if not isinstance(direction, list):
print("Wire.Line - Error: The input direction is not a valid list. Returning None.")
return None
if not len(direction) == 3:
print("Wire.Line - Error: The length of the input direction is not equal to three. Returning None.")
return None
if sides < 2:
print("Wire.Line - Error: The number of sides cannot be less than two. Consider using Edge.Line() instead. Returning None.")
return None
edge = Edge.Line(origin=origin, length=length, direction=direction, placement=placement)
vertices = [Edge.StartVertex(edge)]
unitDistance = float(1)/float(sides)
for i in range(1, sides):
vertices.append(Edge.VertexByParameter(edge, i*unitDistance))
vertices.append(Edge.EndVertex(edge))
return Wire.ByVertices(vertices)
@staticmethod
def OrientEdges(wire, vertexA, tolerance=0.0001):
"""
Returns a correctly oriented head-to-tail version of the input wire. The input wire must be manifold.
Parameters
----------
wire : topologic_core.Wire
The input wire.
vertexA : topologic_core.Vertex
The desired start vertex of the wire.
tolerance : float, optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The oriented wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
if not Topology.IsInstance(wire, "Wire"):
print("Wire.OrientEdges - Error: The input wire parameter is not a valid wire. Returning None.")
return None
if not Topology.IsInstance(vertexA, "Vertex"):
print("Wire.OrientEdges - Error: The input vertexA parameter is not a valid vertex. Returning None.")
return None
if not Wire.IsManifold(wire):
print("Wire.OrientEdges - Error: The input wire parameter is not a manifold wire. Returning None.")
return None
oriented_edges = []
remaining_edges = Topology.Edges(wire)
current_vertex = vertexA
while remaining_edges:
next_edge = None
for edge in remaining_edges:
if Vertex.Distance(Edge.StartVertex(edge), current_vertex) < tolerance:
next_edge = edge
break
elif Vertex.Distance(Edge.EndVertex(edge), current_vertex) < tolerance:
next_edge = Edge.Reverse(edge)
break
if next_edge:
oriented_edges.append(next_edge)
remaining_edges.remove(next_edge)
current_vertex = Edge.EndVertex(next_edge)
else:
# Unable to find a next edge connected to the current vertex
break
vertices = [Edge.StartVertex(oriented_edges[0])]
for i, edge in enumerate(oriented_edges):
vertices.append(Edge.EndVertex(edge))
return Wire.ByVertices(vertices, close=Wire.IsClosed(wire))
@staticmethod
def Planarize(wire, origin= None, mantissa: int = 6, tolerance: float = 0.0001):
"""
Returns a planarized version of the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
tolerance : float, optional
The desired tolerance. The default is 0.0001.
origin : topologic_core.Vertex , optional
The desired origin of the plane unto which the planar wire will be projected. If set to None, the centroid of the input wire will be chosen. The default is None.
mantissa : int , optional
The desired length of the mantissa. The default is 6.
Returns
-------
topologic_core.Wire
The planarized wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not Topology.IsInstance(wire, "Wire"):
print("Wire.Planarize - Error: The input wire parameter is not a valid topologic wire. Returning None.")
return None
if origin == None:
origin = Vertex.Origin()
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
return None
vertices = Topology.Vertices(wire)
edges = Topology.Edges(wire)
plane_equation = Vertex.PlaneEquation(vertices, mantissa=mantissa)
rect = Face.RectangleByPlaneEquation(origin=origin , equation=plane_equation, tolerance=tolerance)
new_vertices = [Vertex.Project(v, rect, mantissa=mantissa) for v in vertices]
new_vertices = Vertex.Fuse(new_vertices, mantissa=mantissa, tolerance=tolerance)
new_edges = []
for edge in edges:
sv = Edge.StartVertex(edge)
ev = Edge.EndVertex(edge)
sv1 = Vertex.Project(sv, rect)
i = Vertex.Index(sv1, new_vertices, tolerance=tolerance)
if i:
sv1 = new_vertices[i]
ev1 = Vertex.Project(ev, rect)
i = Vertex.Index(ev1, new_vertices, tolerance=tolerance)
if i:
ev1 = new_vertices[i]
new_edges.append(Edge.ByVertices([sv1, ev1]))
return Topology.SelfMerge(Cluster.ByTopologies(new_edges), tolerance=tolerance)
@staticmethod
def Project(wire, face, direction: list = None, mantissa: int = 6, tolerance: float = 0.0001):
"""
Creates a projection of the input wire unto the input face.
Parameters
----------
wire : topologic_core.Wire
The input wire.
face : topologic_core.Face
The face unto which to project the input wire.
direction : list, optional
The vector representing the direction of the projection. If None, the reverse vector of the receiving face normal will be used. The default is None.
mantissa : int , optional
The desired length of the mantissa. The default is 6.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The projected wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Face import Face
from topologicpy.Topology import Topology
if not wire:
return None
if not Topology.IsInstance(wire, "Wire"):
return None
if not face:
return None
if not Topology.IsInstance(face, "Face"):
return None
if not direction:
direction = -1*Face.NormalAtParameters(face, 0.5, 0.5, "XYZ", mantissa)
large_face = Topology.Scale(face, face.CenterOfMass(), 500, 500, 500)
edges = []
_ = wire.Edges(None, edges)
projected_edges = []
if large_face:
if (Topology.Type(large_face) == Topology.TypeID("Face")):
for edge in edges:
if edge:
if (Topology.Type(edge) == Topology.TypeID("Edge")):
sv = edge.StartVertex()
ev = edge.EndVertex()
psv = Vertex.Project(vertex=sv, face=large_face, direction=direction)
pev = Vertex.Project(vertex=ev, face=large_face, direction=direction)
if psv and pev:
try:
pe = Edge.ByVertices([psv, pev], tolerance=tolerance)
projected_edges.append(pe)
except:
continue
w = Wire.ByEdges(projected_edges, tolerance=tolerance)
return w
@staticmethod
def Rectangle(origin= None, width: float = 1.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001):
"""
Creates a rectangle.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0).
width : float , optional
The width of the rectangle. The default is 1.0.
length : float , optional
The length of the rectangle. The default is 1.0.
direction : list , optional
The vector representing the up direction of the rectangle. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the rectangle. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
angTolerance : float , optional
The desired angular tolerance. The default is 0.1.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created rectangle.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.Rectangle - Error: specified origin is not a topologic vertex. Retruning None.")
return None
if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]:
print("Wire.Rectangle - Error: Could not find placement in the list of placements. Retruning None.")
return None
width = abs(width)
length = abs(length)
if width < tolerance or length < tolerance:
print("Wire.Rectangle - Error: One or more of the specified dimensions is below the tolerance value. Retruning None.")
return None
if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance:
print("Wire.Rectangle - Error: The direction vector magnitude is below the tolerance value. Retruning None.")
return None
xOffset = 0
yOffset = 0
if placement.lower() == "lowerleft":
xOffset = width*0.5
yOffset = length*0.5
elif placement.lower() == "upperleft":
xOffset = width*0.5
yOffset = -length*0.5
elif placement.lower() == "lowerright":
xOffset = -width*0.5
yOffset = length*0.5
elif placement.lower() == "upperright":
xOffset = -width*0.5
yOffset = -length*0.5
vb1 = Vertex.ByCoordinates(origin.X()-width*0.5+xOffset,origin.Y()-length*0.5+yOffset,origin.Z())
vb2 = Vertex.ByCoordinates(origin.X()+width*0.5+xOffset,origin.Y()-length*0.5+yOffset,origin.Z())
vb3 = Vertex.ByCoordinates(origin.X()+width*0.5+xOffset,origin.Y()+length*0.5+yOffset,origin.Z())
vb4 = Vertex.ByCoordinates(origin.X()-width*0.5+xOffset,origin.Y()+length*0.5+yOffset,origin.Z())
baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], True)
if direction != [0, 0, 1]:
baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction)
return baseWire
@staticmethod
def RemoveCollinearEdges(wire, angTolerance: float = 0.1, tolerance: float = 0.0001):
"""
Removes any collinear edges in the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
angTolerance : float , optional
The desired angular tolerance. The default is 0.1.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created wire without any collinear edges.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Wire import Wire
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
def cleanup(wire, tolerance):
vertices = Topology.Vertices(wire)
vertices = Vertex.Fuse(vertices, tolerance=tolerance)
edges = Topology.Edges(wire)
new_edges = []
for edge in edges:
sv = Edge.StartVertex(edge)
sv = vertices[Vertex.Index(sv, vertices)]
ev = Edge.EndVertex(edge)
ev = vertices[Vertex.Index(ev, vertices)]
new_edges.append(Edge.ByVertices([sv,ev]))
new_wire = Topology.SelfMerge(Cluster.ByTopologies(new_edges), tolerance=tolerance)
return new_wire
def rce(wire, angTolerance=0.1):
if not Topology.IsInstance(wire, "Wire"):
return wire
final_wire = None
vertices = []
wire_verts = []
try:
_ = wire.Vertices(None, vertices)
except:
return wire
for aVertex in vertices:
edges = []
_ = aVertex.Edges(wire, edges)
if len(edges) > 1:
if not Edge.IsCollinear(edges[0], edges[1], tolerance=tolerance):
wire_verts.append(aVertex)
else:
wire_verts.append(aVertex)
if len(wire_verts) > 2:
if wire.IsClosed():
final_wire = Wire.ByVertices(wire_verts, close=True)
else:
final_wire = Wire.ByVertices(wire_verts, close=False)
elif len(wire_verts) == 2:
final_wire = Edge.ByStartVertexEndVertex(wire_verts[0], wire_verts[1], tolerance=tolerance, silent=True)
return final_wire
new_wire = cleanup(wire, tolerance=tolerance)
if not Wire.IsManifold(new_wire):
wires = Wire.Split(new_wire)
else:
wires = [new_wire]
returnWires = []
for aWire in wires:
if not Topology.IsInstance(aWire, "Wire"):
returnWires.append(aWire)
else:
returnWires.append(rce(aWire, angTolerance=angTolerance))
if len(returnWires) == 1:
returnWire = returnWires[0]
if Topology.IsInstance(returnWire, "Edge"):
return Wire.ByEdges([returnWire], tolerance=tolerance)
elif Topology.IsInstance(returnWire, "Wire"):
return returnWire
else:
return wire
elif len(returnWires) > 1:
returnWire = Topology.SelfMerge(Cluster.ByTopologies(returnWires))
if Topology.IsInstance(returnWire, "Edge"):
return Wire.ByEdges([returnWire], tolerance=tolerance)
elif Topology.IsInstance(returnWire, "Wire"):
return returnWire
else:
return wire
else:
return wire
@staticmethod
def Reverse(wire, tolerance: float = 0.0001):
"""
Creates a wire that has the reverse direction of the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The reversed wire.
"""
from topologicpy.Topology import Topology
if not Topology.IsInstance(wire, "Wire"):
print("Wire.Reverse - Error: The input wire parameter is not a valid wire. Returning None.")
return None
if not Wire.IsManifold(wire):
print("Wire.Reverse - Error: The input wire parameter is not a manifold wire. Returning None.")
return None
vertices = Topology.Vertices(wire)
vertices.reverse()
new_wire = Wire.ByVertices(vertices, close=Wire.IsClosed(wire), tolerance=tolerance)
return new_wire
@staticmethod
def Roof(face, angle: float = 45, tolerance: float = 0.001):
"""
Creates a hipped roof through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
Parameters
----------
face : topologic_core.Face
The input face.
angle : float , optioal
The desired angle in degrees of the roof. The default is 45.
tolerance : float , optional
The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
Returns
-------
topologic_core.Wire
The created roof. This method returns the roof as a set of edges. No faces are created.
"""
from topologicpy import Polyskel
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Dictionary import Dictionary
from topologicpy.Helper import Helper
import topologic_core as topologic
import math
def subtrees_to_edges(subtrees, polygon, slope):
polygon_z = {}
for x, y, z in polygon:
polygon_z[(x, y)] = z
edges = []
for subtree in subtrees:
source = subtree.source
height = subtree.height
z = slope * height
source_vertex = Vertex.ByCoordinates(source.x, source.y, z)
for sink in subtree.sinks:
if (sink.x, sink.y) in polygon_z:
z = 0
else:
z = None
for st in subtrees:
if st.source.x == sink.x and st.source.y == sink.y:
z = slope * st.height
break
for sk in st.sinks:
if sk.x == sink.x and sk.y == sink.y:
z = slope * st.height
break
if z is None:
height = subtree.height
z = slope * height
sink_vertex = Vertex.ByCoordinates(sink.x, sink.y, z)
if (source.x, source.y) == (sink.x, sink.y):
continue
e = Edge.ByStartVertexEndVertex(source_vertex, sink_vertex, tolerance=tolerance, silent=True)
if e not in edges and e != None:
edges.append(e)
return edges
def face_to_skeleton(face, angle=0):
normal = Face.Normal(face)
eb_wire = Face.ExternalBoundary(face)
ib_wires = Face.InternalBoundaries(face)
eb_vertices = Topology.Vertices(eb_wire)
if normal[2] > 0:
eb_vertices = list(reversed(eb_vertices))
eb_polygon_coordinates = [(v.X(), v.Y(), v.Z()) for v in eb_vertices]
eb_polygonxy = [(x[0], x[1]) for x in eb_polygon_coordinates]
ib_polygonsxy = []
zero_coordinates = eb_polygon_coordinates
for ib_wire in ib_wires:
ib_vertices = Topology.Vertices(ib_wire)
if normal[2] > 0:
ib_vertices = list(reversed(ib_vertices))
ib_polygon_coordinates = [(v.X(), v.Y(), v.Z()) for v in ib_vertices]
ib_polygonxy = [(x[0], x[1]) for x in ib_polygon_coordinates]
ib_polygonsxy.append(ib_polygonxy)
zero_coordinates += ib_polygon_coordinates
skeleton = Polyskel.skeletonize(eb_polygonxy, ib_polygonsxy)
slope = math.tan(math.radians(angle))
roofEdges = subtrees_to_edges(skeleton, zero_coordinates, slope)
roofEdges = Helper.Flatten(roofEdges)+Topology.Edges(face)
roofTopology = Topology.SelfMerge(Cluster.ByTopologies(roofEdges), tolerance=tolerance)
return roofTopology
if not Topology.IsInstance(face, "Face"):
return None
angle = abs(angle)
if angle >= 90-tolerance:
return None
origin = Topology.Centroid(face)
normal = Face.Normal(face)
flat_face = Topology.Flatten(face, origin=origin, direction=normal)
d = Topology.Dictionary(flat_face)
roof = face_to_skeleton(flat_face, angle)
if not roof:
return None
roof = Topology.Unflatten(roof, origin=origin, direction=normal)
return roof
@staticmethod
def Simplify(wire, tolerance=0.0001):
"""
Simplifies the input wire edges based on the Douglas Peucker algorthim. See https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
Part of this code was contributed by gaoxipeng. See https://github.com/wassimj/topologicpy/issues/35
Parameters
----------
wire : topologic_core.Wire
The input wire.
tolerance : float , optional
The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed.
Returns
-------
topologic_core.Wire
The simplified wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
def perpendicular_distance(point, line_start, line_end):
# Calculate the perpendicular distance from a point to a line segment
x0 = point.X()
y0 = point.Y()
x1 = line_start.X()
y1 = line_start.Y()
x2 = line_end.X()
y2 = line_end.Y()
numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
denominator = Vertex.Distance(line_start, line_end)
return numerator / denominator
def douglas_peucker(wire, tolerance):
if isinstance(wire, list):
points = wire
else:
points = Wire.Vertices(wire)
# points.insert(0, points.pop())
if len(points) <= 2:
return points
# Use the first and last points in the list as the starting and ending points
start_point = points[0]
end_point = points[-1]
# Find the point with the maximum distance
max_distance = 0
max_index = 0
for i in range(1, len(points) - 1):
d = perpendicular_distance(points[i], start_point, end_point)
if d > max_distance:
max_distance = d
max_index = i
# If the maximum distance is less than the tolerance, no further simplification is needed
if max_distance <= tolerance:
return [start_point, end_point]
# Recursively simplify
first_segment = douglas_peucker(points[:max_index + 1], tolerance)
second_segment = douglas_peucker(points[max_index:], tolerance)
# Merge the two simplified segments
return first_segment[:-1] + second_segment
if not Topology.IsInstance(wire, "Wire"):
print("Wire.Simplify = Error: The input wire parameter is not a Wire. Returning None.")
return None
if not Wire.IsManifold(wire):
wires = Wire.Split(wire)
new_wires = []
for w in wires:
if Topology.IsInstance(w, "Edge"):
if Edge.Length(w) > tolerance:
new_wires.append(w)
elif Topology.IsInstance(w, "Wire"):
new_wires.append(Wire.Simplify(w, tolerance=tolerance))
return_wire = Topology.SelfMerge(Cluster.ByTopologies(new_wires))
return return_wire
new_vertices = douglas_peucker(wire, tolerance=tolerance)
new_wire = Wire.ByVertices(new_vertices, close=Wire.IsClosed(wire))
return new_wire
@staticmethod
def Skeleton(face, tolerance=0.001):
"""
Creates a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
Parameters
----------
face : topologic_core.Face
The input face.
tolerance : float , optional
The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
Returns
-------
topologic_core.Wire
The created straight skeleton.
"""
if not Topology.IsInstance(face, "Face"):
return None
return Wire.Roof(face, angle=0, tolerance=tolerance)
@staticmethod
def Spiral(origin = None, radiusA : float = 0.05, radiusB : float = 0.5, height : float = 1, turns : int = 10, sides : int = 36, clockwise : bool = False, reverse : bool = False, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates a spiral.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the spiral. The default is None which results in the spiral being placed at (0, 0, 0).
radiusA : float , optional
The initial radius of the spiral. The default is 0.05.
radiusB : float , optional
The final radius of the spiral. The default is 0.5.
height : float , optional
The height of the spiral. The default is 1.
turns : int , optional
The number of turns of the spiral. The default is 10.
sides : int , optional
The number of sides of one full turn in the spiral. The default is 36.
clockwise : bool , optional
If set to True, the spiral will be oriented in a clockwise fashion. Otherwise, it will be oriented in an anti-clockwise fashion. The default is False.
reverse : bool , optional
If set to True, the spiral will increase in height from the center to the circumference. Otherwise, it will increase in height from the conference to the center. The default is False.
direction : list , optional
The vector representing the up direction of the spiral. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the spiral. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
Returns
-------
topologic_core.Wire
The created spiral.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Topology import Topology
import math
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.Spiral - Error: the input origin is not a valid topologic Vertex. Returning None.")
return None
if radiusA <= 0:
print("Wire.Spiral - Error: the input radiusA cannot be less than or equal to zero. Returning None.")
return None
if radiusB <= 0:
print("Wire.Spiral - Error: the input radiusB cannot be less than or equal to zero. Returning None.")
return None
if radiusA == radiusB:
print("Wire.Spiral - Error: the inputs radiusA and radiusB cannot be equal. Returning None.")
return None
if radiusB > radiusA:
temp = radiusA
radiusA = radiusB
radiusB = temp
if turns <= 0:
print("Wire.Spiral - Error: the input turns cannot be less than or equal to zero. Returning None.")
return None
if sides < 3:
print("Wire.Spiral - Error: the input sides cannot be less than three. Returning None.")
return None
if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]:
print("Wire.Spiral - Error: the input placement string is not one of center, lowerleft, upperleft, lowerright, or upperright. Returning None.")
return None
if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance:
print("Wire.Spiral - Error: the input direction vector is not a valid direction. Returning None.")
return None
vertices = []
xList = []
yList = []
zList = []
if clockwise:
cw = -1
else:
cw = 1
n_vertices = sides*turns + 1
zOffset = height/float(n_vertices)
if reverse == True:
z = height
else:
z = 0
ang = 0
angOffset = float(360/float(sides))
b = (radiusB - radiusA)/(2*math.pi*turns)
while ang <= 360*turns:
rad = math.radians(ang)
x = (radiusA + b*rad)*math.cos(rad)*cw
xList.append(x)
y = (radiusA + b*rad)*math.sin(rad)
yList.append(y)
zList.append(z)
if reverse == True:
z = z - zOffset
else:
z = z + zOffset
vertices.append(Vertex.ByCoordinates(x, y, z))
ang = ang + angOffset
minX = min(xList)
maxX = max(xList)
minY = min(yList)
maxY = max(yList)
radius = radiusA + radiusB*turns*0.5
baseWire = Wire.ByVertices(vertices, close=False)
if placement.lower() == "center":
baseWire = Topology.Translate(baseWire, 0, 0, -height*0.5)
if placement.lower() == "lowerleft":
baseWire = Topology.Translate(baseWire, -minX, -minY, 0)
elif placement.lower() == "upperleft":
baseWire = Topology.Translate(baseWire, -minX, -maxY, 0)
elif placement.lower() == "lowerright":
baseWire = Topology.Translate(baseWire, -maxX, -minY, 0)
elif placement.lower() == "upperright":
baseWire = Topology.Translate(baseWire, -maxX, -maxY, 0)
if direction != [0, 0, 1]:
baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction)
return baseWire
@staticmethod
def Split(wire) -> list:
"""
Splits the input wire into segments at its intersections (i.e. at any vertex where more than two edges meet).
Parameters
----------
wire : topologic_core.Wire
The input wire.
Returns
-------
list
The list of split wire segments.
"""
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
def vertexDegree(v, wire):
edges = []
_ = v.Edges(wire, edges)
return len(edges)
def vertexOtherEdge(vertex, edge, wire):
edges = []
_ = vertex.Edges(wire, edges)
if Topology.IsSame(edges[0], edge):
return edges[-1]
else:
return edges[0]
def edgeOtherVertex(edge, vertex):
vertices = []
_ = edge.Vertices(None, vertices)
if Topology.IsSame(vertex, vertices[0]):
return vertices[-1]
else:
return vertices[0]
def edgeInList(edge, edgeList):
for anEdge in edgeList:
if Topology.IsSame(anEdge, edge):
return True
return False
vertices = []
_ = wire.Vertices(None, vertices)
hubs = []
for aVertex in vertices:
if vertexDegree(aVertex, wire) > 2:
hubs.append(aVertex)
wires = []
global_edges = []
for aVertex in hubs:
hub_edges = []
_ = aVertex.Edges(wire, hub_edges)
wire_edges = []
for hub_edge in hub_edges:
if not edgeInList(hub_edge, global_edges):
current_edge = hub_edge
oe = edgeOtherVertex(current_edge, aVertex)
while vertexDegree(oe, wire) == 2:
if not edgeInList(current_edge, global_edges):
global_edges.append(current_edge)
wire_edges.append(current_edge)
current_edge = vertexOtherEdge(oe, current_edge, wire)
oe = edgeOtherVertex(current_edge, oe)
if not edgeInList(current_edge, global_edges):
global_edges.append(current_edge)
wire_edges.append(current_edge)
if len(wire_edges) > 1:
wires.append(Cluster.ByTopologies(wire_edges).SelfMerge())
else:
wires.append(wire_edges[0])
wire_edges = []
if len(wires) < 1:
return [wire]
return wires
@staticmethod
def Square(origin= None, size: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates a square.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0).
size : float , optional
The size of the square. The default is 1.0.
direction : list , optional
The vector representing the up direction of the square. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the square. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created square.
"""
return Wire.Rectangle(origin=origin, width=size, length=size, direction=direction, placement=placement, tolerance=tolerance)
@staticmethod
def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.0, b: float = 2.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001):
"""
Creates a Squircle which is a hybrid between a circle and a square. See https://en.wikipedia.org/wiki/Squircle
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the squircle. The default is None which results in the squircle being placed at (0, 0, 0).
radius : float , optional
The radius of the squircle. The default is 0.5.
sides : int , optional
The number of sides of the squircle. The default is 121.
a : float , optional
The "a" factor affects the x position of the points to interpolate between a circle and a square.
A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
b : float , optional
The "b" factor affects the y position of the points to interpolate between a circle and a square.
A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
radius : float , optional
The desired radius of the squircle. The default is 0.5.
sides : int , optional
The desired number of sides for the squircle. The default is 100.
direction : list , optional
The vector representing the up direction of the circle. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
angTolerance : float , optional
The desired angular tolerance. The default is 0.1.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created squircle.
"""
def get_squircle(a=1, b=1, radius=0.5, sides=100):
import numpy as np
t = np.linspace(0, 2*np.pi, sides)
x = (np.abs(np.cos(t))**(1/a)) * np.sign(np.cos(t))
y = (np.abs(np.sin(t))**(1/b)) * np.sign(np.sin(t))
return x*radius, y*radius
from topologicpy.Vertex import Vertex
from topologicpy.Wire import Wire
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.Squircle - Error: The input origin parameter is not a valid Vertex. Retruning None.")
return None
if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]:
print("Wire.Squircle - Error: The input placement parameter is not a recognised string. Retruning None.")
return None
radius = abs(radius)
if radius < tolerance:
return None
if a <= 0:
print("Wire.Squircle - Error: The a input parameter must be a positive number. Returning None.")
return None
if b <= 0:
print("Wire.Squircle - Error: The b input parameter must be a positive number. Returning None.")
return None
if a == 1 and b == 1:
return Wire.Circle(radius=radius, sides=sides, direction=direction, placement=placement, tolerance=tolerance)
x_list, y_list = get_squircle(a=a, b=b, radius=radius, sides=sides)
vertices = []
for i, x in enumerate(x_list):
v = Vertex.ByCoordinates(x, y_list[i], 0)
vertices.append(v)
baseWire = Wire.ByVertices(vertices, close=True)
baseWire = Topology.RemoveCollinearEdges(baseWire, angTolerance=angTolerance, tolerance=tolerance)
baseWire = Wire.Simplify(baseWire, tolerance=tolerance)
if placement.lower() == "lowerleft":
baseWire = Topology.Translate(baseWire, radius, radius, 0)
elif placement.lower() == "upperleft":
baseWire = Topology.Translate(baseWire, radius, -radius, 0)
elif placement.lower() == "lowerright":
baseWire = Topology.Translate(baseWire, -radius, radius, 0)
elif placement.lower() == "upperright":
baseWire = Topology.Translate(baseWire, -radius, -radius, 0)
if direction != [0, 0, 1]:
baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction)
return baseWire
@staticmethod
def Star(origin= None, radiusA: float = 0.5, radiusB: float = 0.2, rays: int = 8, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates a star.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the star. The default is None which results in the star being placed at (0, 0, 0).
radiusA : float , optional
The outer radius of the star. The default is 1.0.
radiusB : float , optional
The outer radius of the star. The default is 0.4.
rays : int , optional
The number of star rays. The default is 8.
direction : list , optional
The vector representing the up direction of the star. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the star. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created star.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
return None
radiusA = abs(radiusA)
radiusB = abs(radiusB)
if radiusA < tolerance or radiusB < tolerance:
return None
rays = abs(rays)
if rays < 3:
return None
if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]:
return None
sides = rays*2 # Sides is double the number of rays
baseV = []
xList = []
yList = []
for i in range(sides):
if i%2 == 0:
radius = radiusA
else:
radius = radiusB
angle = math.radians(360/sides)*i
x = math.sin(angle)*radius + origin.X()
y = math.cos(angle)*radius + origin.Y()
z = origin.Z()
xList.append(x)
yList.append(y)
baseV.append([x, y])
if placement.lower() == "lowerleft":
xmin = min(xList)
ymin = min(yList)
xOffset = origin.X() - xmin
yOffset = origin.Y() - ymin
elif placement.lower() == "upperleft":
xmin = min(xList)
ymax = max(yList)
xOffset = origin.X() - xmin
yOffset = origin.Y() - ymax
elif placement.lower() == "lowerright":
xmax = max(xList)
ymin = min(yList)
xOffset = origin.X() - xmax
yOffset = origin.Y() - ymin
elif placement.lower() == "upperright":
xmax = max(xList)
ymax = max(yList)
xOffset = origin.X() - xmax
yOffset = origin.Y() - ymax
else:
xOffset = 0
yOffset = 0
tranBase = []
for coord in baseV:
tranBase.append(Vertex.ByCoordinates(coord[0]+xOffset, coord[1]+yOffset, origin.Z()))
baseWire = Wire.ByVertices(tranBase[::-1], True) #reversing the list so that the normal points up in Blender
if direction != [0, 0, 1]:
baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction)
return baseWire
@staticmethod
def StartEndVertices(wire) -> list:
"""
Returns the start and end vertices of the input wire. The wire must be manifold and open.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Topology import Topology
if not Wire.IsManifold(wire):
print("Wire.StartEndVertices - Error: The input wire parameter is non-manifold. Returning None.")
return None
vertices = Topology.Vertices(wire)
if Wire.IsClosed(wire):
return [vertices[0], vertices[0]] # If the wire is closed, the start and end vertices are the same vertex
endPoints = [v for v in vertices if (Vertex.Degree(v, wire) == 1)]
if len(endPoints) < 2:
print("Wire.StartEndVertices - Error: Could not find the end vertices if the input wire parameter. Returning None.")
return None
edge1 = Topology.SuperTopologies(endPoints[0], wire, topologyType="edge")[0]
sv = Edge.StartVertex(edge1)
if (Topology.IsSame(endPoints[0], sv)):
wireStartVertex = endPoints[0]
wireEndVertex = endPoints[1]
else:
wireStartVertex = endPoints[1]
wireEndVertex = endPoints[0]
return [wireStartVertex, wireEndVertex]
@staticmethod
def StartVertex(wire):
"""
Returns the start vertex of the input wire. The wire must be manifold and open.
"""
sv, ev = Wire.StartEndVertices(wire)
return sv
@staticmethod
def Trapezoid(origin= None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates a trapezoid.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the trapezoid. The default is None which results in the trapezoid being placed at (0, 0, 0).
widthA : float , optional
The width of the bottom edge of the trapezoid. The default is 1.0.
widthB : float , optional
The width of the top edge of the trapezoid. The default is 0.75.
offsetA : float , optional
The offset of the bottom edge of the trapezoid. The default is 0.0.
offsetB : float , optional
The offset of the top edge of the trapezoid. The default is 0.0.
length : float , optional
The length of the trapezoid. The default is 1.0.
direction : list , optional
The vector representing the up direction of the trapezoid. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the trapezoid. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire
The created trapezoid.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
return None
widthA = abs(widthA)
widthB = abs(widthB)
length = abs(length)
if widthA < tolerance or widthB < tolerance or length < tolerance:
return None
if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]:
return None
xOffset = 0
yOffset = 0
if placement.lower() == "center":
xOffset = -((-widthA*0.5 + offsetA) + (-widthB*0.5 + offsetB) + (widthA*0.5 + offsetA) + (widthB*0.5 + offsetB))/4.0
yOffset = 0
elif placement.lower() == "lowerleft":
xOffset = -(min((-widthA*0.5 + offsetA), (-widthB*0.5 + offsetB)))
yOffset = length*0.5
elif placement.lower() == "upperleft":
xOffset = -(min((-widthA*0.5 + offsetA), (-widthB*0.5 + offsetB)))
yOffset = -length*0.5
elif placement.lower() == "lowerright":
xOffset = -(max((widthA*0.5 + offsetA), (widthB*0.5 + offsetB)))
yOffset = length*0.5
elif placement.lower() == "upperright":
xOffset = -(max((widthA*0.5 + offsetA), (widthB*0.5 + offsetB)))
yOffset = -length*0.5
vb1 = Vertex.ByCoordinates(origin.X()-widthA*0.5+offsetA+xOffset,origin.Y()-length*0.5+yOffset,origin.Z())
vb2 = Vertex.ByCoordinates(origin.X()+widthA*0.5+offsetA+xOffset,origin.Y()-length*0.5+yOffset,origin.Z())
vb3 = Vertex.ByCoordinates(origin.X()+widthB*0.5+offsetB+xOffset,origin.Y()+length*0.5+yOffset,origin.Z())
vb4 = Vertex.ByCoordinates(origin.X()-widthB*0.5++offsetB+xOffset,origin.Y()+length*0.5+yOffset,origin.Z())
baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], True)
if direction != [0, 0, 1]:
baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction)
return baseWire
@staticmethod
def VertexDistance(wire, vertex, origin= None, mantissa: int = 6, tolerance: float = 0.0001):
"""
Returns the distance, computed along the input wire of the input vertex from the input origin vertex.
Parameters
----------
wire : topologic_core.Wire
The input wire.
vertex : topologic_core.Vertex
The input vertex
origin : topologic_core.Vertex , optional
The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input wire. The default is None.
mantissa : int , optional
The desired length of the mantissa. The default is 6.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
float
The distance of the input vertex from the input origin along the input wire.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Topology import Topology
if not Topology.IsInstance(wire, "Wire"):
print("Wire.VertexDistance - Error: The input wire parameter is not a valid topologic wire. Returning None.")
return None
if not Topology.IsInstance(vertex, "Vertex"):
print("Wire.VertexDistance - Error: The input vertex parameter is not a valid topologic vertex. Returning None.")
return None
wire_length = Wire.Length(wire)
if wire_length < tolerance:
print("Wire.VertexDistance: The input wire parameter is a degenerate topologic wire. Returning None.")
return None
if origin == None:
origin = Wire.StartVertex(wire)
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.VertexDistance - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
return None
if not Vertex.IsInternal(vertex, wire, tolerance=tolerance):
print("Wire.VertexDistance: The input vertex parameter is not internal to the input wire parameter. Returning None.")
return None
def distance_from_start(wire, v):
total_distance = 0.0
found = False
# Iterate over the edges of the wire
for edge in Wire.Edges(wire):
if Vertex.IsInternal(v, edge, tolerance=tolerance):
total_distance += Vertex.Distance(Edge.StartVertex(edge), v)
found = True
break
total_distance += Edge.Length(edge)
if found == False:
return None
return total_distance
d1 = distance_from_start(wire, vertex)
d2 = distance_from_start(wire, origin)
if d1 == None:
print("Wire.VertexDistance - Error: The input vertex parameter is not internal to the input wire parameter. Returning None.")
return None
if d2 == None:
print("Wire.VertexDistance - Error: The input origin parameter is not internal to the input wire parameter. Returning None.")
return None
return round(abs(d2-d1), mantissa)
@staticmethod
def VertexByDistance(wire, distance: float = 0.0, origin= None, tolerance = 0.0001):
"""
Creates a vertex along the input wire offset by the input distance from the input origin.
Parameters
----------
edge : topologic_core.Edge
The input edge.
distance : float , optional
The offset distance. The default is 0.
origin : topologic_core.Vertex , optional
The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input edge. The default is None.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Vertex
The created vertex.
"""
from topologicpy.Vertex import Vertex
def compute_u(u):
def count_decimal_places(number):
try:
# Convert the number to a string to analyze decimal places
num_str = str(number)
# Split the number into integer and decimal parts
integer_part, decimal_part = num_str.split('.')
# Return the length of the decimal part
return len(decimal_part)
except ValueError:
# If there's no decimal part, return 0
return 0
dp = count_decimal_places(u)
u = -(int(u) - u)
return round(u,dp)
if not Topology.IsInstance(wire, "Wire"):
print("Wire.VertexByDistance - Error: The input wire parameter is not a valid topologic wire. Returning None.")
return None
wire_length = Wire.Length(wire)
if wire_length < tolerance:
print("Wire.VertexByDistance: The input wire parameter is a degenerate topologic wire. Returning None.")
return None
if abs(distance) < tolerance:
return Wire.StartVertex(wire)
if abs(distance - wire_length) < tolerance:
return Wire.EndVertex(wire)
if not Wire.IsManifold(wire):
print("Wire.VertexAtParameter - Error: The input wire parameter is non-manifold. Returning None.")
return None
if origin == None:
origin = Wire.StartVertex(wire)
if not Topology.IsInstance(origin, "Vertex"):
print("Wire.VertexByDistance - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
return None
if not Vertex.IsInternal(origin, wire, tolerance=tolerance):
print("Wire.VertexByDistance - Error: The input origin parameter is not internal to the input wire parameter. Returning None.")
return None
if Vertex.Distance(Wire.StartVertex(wire), origin) < tolerance:
u = distance/wire_length
elif Vertex.Distance(Wire.EndVertex(wire), origin) < tolerance:
u = 1 - distance/wire_length
else:
d = Wire.VertexDistance(wire, origin) + distance
u = d/wire_length
return Wire.VertexByParameter(wire, u=compute_u(u))
@staticmethod
def VertexByParameter(wire, u: float = 0):
"""
Creates a vertex along the input wire offset by the input *u* parameter. The wire must be manifold.
Parameters
----------
wire : topologic_core.Wire
The input wire.
u : float , optional
The *u* parameter along the input topologic Wire. A parameter of 0 returns the start vertex. A parameter of 1 returns the end vertex. The default is 0.
Returns
-------
topologic_core.Vertex
The vertex at the input u parameter
"""
from topologicpy.Edge import Edge
if not Topology.IsInstance(wire, "Wire"):
print("Wire.VertexAtParameter - Error: The input wire parameter is not a valid topologic wire. Returning None.")
return None
if u < 0 or u > 1:
print("Wire.VertexAtParameter - Error: The input u parameter is not within the valid range of [0, 1]. Returning None.")
return None
if not Wire.IsManifold(wire):
print("Wire.VertexAtParameter - Error: The input wire parameter is non-manifold. Returning None.")
return None
if u == 0:
return Wire.StartVertex(wire)
if u == 1:
return Wire.EndVertex(wire)
edges = Wire.Edges(wire)
total_length = 0.0
edge_lengths = []
# Compute the total length of the wire
for edge in edges:
e_length = Edge.Length(edge)
edge_lengths.append(e_length)
total_length += e_length
# Initialize variables for tracking the current edge and accumulated length
current_edge = None
accumulated_length = 0.0
# Iterate over the lines to find the appropriate segment
for i, edge in enumerate(edges):
edge_length = edge_lengths[i]
# Check if the desired point is on this line
if u * total_length <= accumulated_length + edge_length:
current_edge = edge
break
else:
accumulated_length += edge_length
# Calculate the residual u value for the current line
residual_u = (u * total_length - accumulated_length) / Edge.Length(current_edge)
# Compute the point at the parameter on the current line
vertex = Edge.VertexByParameter(current_edge, residual_u)
return vertex
@staticmethod
def Vertices(wire) -> list:
"""
Returns the list of vertices of the input wire.
Parameters
----------
wire : topologic_core.Wire
The input wire.
Returns
-------
list
The list of vertices.
"""
if not Topology.IsInstance(wire, "Wire"):
return None
vertices = []
_ = wire.Vertices(None, vertices)
return vertices
Classes
class Wire
-
Expand source code
class Wire(Topology): @staticmethod def Arc(startVertex, middleVertex, endVertex, sides: int = 16, close: bool = True, tolerance: float = 0.0001): """ Creates an arc. The base chord will be parallel to the x-axis and the height will point in the positive y-axis direction. Parameters ---------- startVertex : topologic_core.Vertex The location of the start vertex of the arc. middleVertex : topologic_core.Vertex The location of the middle vertex (apex) of the arc. endVertex : topologic_core.Vertex The location of the end vertex of the arc. sides : int , optional The number of sides of the arc. The default is 16. close : bool , optional If set to True, the arc will be closed by connecting the last vertex to the first vertex. Otherwise, it will be left open. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created arc. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Topology import Topology def segmented_arc(x1, y1, x2, y2, x3, y3, sides): import math """ Generates a segmented arc passing through the three given points. Arguments: x1, y1: Coordinates of the first point x2, y2: Coordinates of the second point x3, y3: Coordinates of the third point sides: Number of sides to divide the arc Returns: List of tuples [x, y] representing the segmented arc passing through the points """ # Calculate the center of the circle A = x2 - x1 B = y2 - y1 C = x3 - x1 D = y3 - y1 E = A * (x1 + x2) + B * (y1 + y2) F = C * (x1 + x3) + D * (y1 + y3) G = 2 * (A * (y3 - y2) - B * (x3 - x2)) if G == 0: center_x = 0 center_y = 0 else: center_x = (D * E - B * F) / G center_y = (A * F - C * E) / G # Calculate the radius of the circle radius = math.sqrt((center_x - x1) ** 2 + (center_y - y1) ** 2) # Calculate the angles between the center and the three points angle1 = math.atan2(y1 - center_y, x1 - center_x) angle3 = math.atan2(y3 - center_y, x3 - center_x) # Calculate the angle between points 1 and 3 angle13 = (angle3 - angle1) % (2 * math.pi) if angle13 < 0: angle13 += 2 * math.pi # Determine the direction of the arc based on the angle between points 1 and 3 if angle13 < math.pi: start_angle = angle3 end_angle = angle1 else: start_angle = angle1 end_angle = angle3 # Calculate the angle increment angle_increment = (end_angle - start_angle) / sides # Generate the points of the arc passing through the points arc_points = [] for i in range(sides + 1): angle = start_angle + i * angle_increment x = center_x + radius * math.cos(angle) y = center_y + radius * math.sin(angle) arc_points.append([x, y]) return arc_points if not Topology.IsInstance(startVertex, "Vertex"): print("Wire.Arc - Error: The startVertex parameter is not a valid vertex. Returning None.") return None if not Topology.IsInstance(middleVertex, "Vertex"): print("Wire.Arc - Error: The middleVertex parameter is not a valid vertex. Returning None.") return None if not Topology.IsInstance(endVertex, "Vertex"): print("Wire.Arc - Error: The endVertex parameter is not a valid vertex. Returning None.") return None if Vertex.AreCollinear([startVertex, middleVertex, endVertex], tolerance = tolerance): return Wire.ByVertices([startVertex, middleVertex, endVertex], close=False) w = Wire.ByVertices([startVertex, middleVertex, endVertex], close=True) f = Face.ByWire(w, tolerance=tolerance) normal = Face.Normal(f) flat_w = Topology.Flatten(w, origin=startVertex, direction=normal) v1, v2, v3 = Topology.Vertices(flat_w) x1, y1, z1 = Vertex.Coordinates(v1) x2, y2, z2 = Vertex.Coordinates(v2) x3, y3, z3 = Vertex.Coordinates(v3) arc_points = segmented_arc(x1, y1, x2, y2, x3, y3, sides) arc_verts = [Vertex.ByCoordinates(coord[0], coord[1], 0) for coord in arc_points] arc = Wire.ByVertices(arc_verts, close=close) # Unflatten the arc arc = Topology.Unflatten(arc, origin=startVertex, direction=normal) return arc @staticmethod def BoundingRectangle(topology, optimize: int = 0, tolerance=0.0001): """ Returns a wire representing a bounding rectangle of the input topology. The returned wire contains a dictionary with key "zrot" that represents rotations around the Z axis. If applied the resulting wire will become axis-aligned. Parameters ---------- topology : topologic_core.Topology The input topology. optimize : int , optional If set to an integer from 1 (low optimization) to 10 (high optimization), the method will attempt to optimize the bounding rectangle so that it reduces its surface area. The default is 0 which will result in an axis-aligned bounding rectangle. The default is 0. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The bounding rectangle of the input topology. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import sample import time def br(topology): vertices = [] _ = topology.Vertices(None, vertices) x = [] y = [] for aVertex in vertices: x.append(aVertex.X()) y.append(aVertex.Y()) minX = min(x) minY = min(y) maxX = max(x) maxY = max(y) return [minX, minY, maxX, maxY] if not Topology.IsInstance(topology, "Topology"): return None world_origin = Vertex.Origin() vertices = Topology.SubTopologies(topology=topology, subTopologyType="vertex") start = time.time() period = 0 result = True while result and period < 30: vList = sample(vertices, 3) result = Vertex.AreCollinear(vList) end = time.time() period = end - start if result == True: print("Wire.BoundingRectangle - Error: Could not find three vertices that are not colinear within 30 seconds. Returning None.") return None w = Wire.ByVertices(vList) f = Face.ByWire(w, tolerance=tolerance) f_origin = Topology.Centroid(f) normal = Face.Normal(f) topology = Topology.Flatten(topology, origin=f_origin, direction=normal) boundingRectangle = br(topology) minX = boundingRectangle[0] minY = boundingRectangle[1] maxX = boundingRectangle[2] maxY = boundingRectangle[3] w = abs(maxX - minX) l = abs(maxY - minY) best_area = l*w orig_area = best_area best_z = 0 best_br = boundingRectangle origin = Topology.Centroid(topology) optimize = min(max(optimize, 0), 10) if optimize > 0: factor = (round(((11 - optimize)/30 + 0.57), 2)) flag = False for n in range(10, 0, -1): if flag: break za = n zb = 90+n zc = n for z in range(za,zb,zc): if flag: break t = Topology.Rotate(topology, origin=origin, axis=[0, 0, 1], angle=z) minX, minY, maxX, maxY = br(t) w = abs(maxX - minX) l = abs(maxY - minY) area = l*w if area < orig_area*factor: best_area = area best_z = z best_br = [minX, minY, maxX, maxY] flag = True break if area < best_area: best_area = area best_z = z best_br = [minX, minY, maxX, maxY] else: best_br = boundingRectangle minX, minY, maxX, maxY = best_br vb1 = Vertex.ByCoordinates(minX, minY, 0) vb2 = Vertex.ByCoordinates(maxX, minY, 0) vb3 = Vertex.ByCoordinates(maxX, maxY, 0) vb4 = Vertex.ByCoordinates(minX, maxY, 0) boundingRectangle = Wire.ByVertices([vb1, vb2, vb3, vb4], close=True) boundingRectangle = Topology.Rotate(boundingRectangle, origin=origin, axis=[0, 0, 1], angle=-best_z) boundingRectangle = Topology.Unflatten(boundingRectangle, origin=f_origin, direction=normal) dictionary = Dictionary.ByKeysValues(["zrot"], [best_z]) boundingRectangle = Topology.SetDictionary(boundingRectangle, dictionary) return boundingRectangle @staticmethod def ByEdges(edges: list, orient: bool = False, tolerance: float = 0.0001): """ Creates a wire from the input list of edges. Parameters ---------- edges : list The input list of edges. orient : bool , optional If set to True the edges are oriented head to tail. Otherwise, they are not. The default is False. tolerance : float , optional The desired tolerance. The default is 0.0001 Returns ------- topologic_core.Wire The created wire. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not isinstance(edges, list): return None edgeList = [x for x in edges if Topology.IsInstance(x, "Edge")] if len(edgeList) == 0: print("Wire.ByEdges - Error: The input edges list does not contain any valid edges. Returning None.") return None if len(edgeList) == 1: wire = topologic.Wire.ByEdges(edgeList) # Hook to Core else: wire = Topology.SelfMerge(Cluster.ByTopologies(edgeList), tolerance=tolerance) if not Topology.IsInstance(wire, "Wire"): print("Wire.ByEdges - Error: The operation failed. Returning None.") wire = None if Wire.IsManifold(wire): if orient == True: wire = Wire.OrientEdges(wire, Wire.StartVertex(wire), tolerance=tolerance) return wire @staticmethod def ByEdgesCluster(cluster, tolerance: float = 0.0001): """ Creates a wire from the input cluster of edges. Parameters ---------- cluster : topologic_core.Cluster The input cluster of edges. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire. """ if not Topology.IsInstance(cluster, "Cluster"): print("Wire.ByEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.") return None edges = [] _ = cluster.Edges(None, edges) return Wire.ByEdges(edges, tolerance=tolerance) @staticmethod def ByOffset(wire, offset: float = 1.0, miter: bool = False, miterThreshold: float = None, offsetKey: str = None, miterThresholdKey: str = None, step: bool = True, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates an offset wire from the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. offset : float , optional The desired offset distance. The default is 1.0. miter : bool , optional if set to True, the corners will be mitered. The default is False. miterThreshold : float , optional The distance beyond which a miter should be added. The default is None which means the miter threshold is set to the offset distance multiplied by the square root of 2. offsetKey : str , optional If specified, the dictionary of the edges will be queried for this key to specify the desired offset. The default is None. miterThresholdKey : str , optional If specified, the dictionary of the vertices will be queried for this key to specify the desired miter threshold distance. The default is None. step : bool , optional If set to True, The transition between collinear edges with different offsets will be a step. Otherwise, it will be a continous edge. The default is True. angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import randrange, sample if not Topology.IsInstance(wire, "Wire"): return None if not miterThreshold: miterThreshold = offset*math.sqrt(2) flatFace = Face.ByWire(wire, tolerance=tolerance) origin = Topology.Centroid(flatFace) normal = Face.Normal(flatFace) flatFace = Topology.Flatten(flatFace, origin=origin, direction=normal) edges = Wire.Edges(wire) vertices = Wire.Vertices(wire) flatEdges = [] flatVertices = [] newEdges = [] for i in range(len(vertices)): flatVertex = Topology.Flatten(vertices[i], origin=origin, direction=normal) flatVertices.append(flatVertex) vertices = flatVertices for i in range(len(edges)): flatEdge = Topology.Flatten(edges[i], origin=origin, direction=normal) flatEdges.append(flatEdge) if offsetKey: d = Topology.Dictionary(edges[i]) value = Dictionary.ValueAtKey(d, key=offsetKey) c = Topology.Centroid(flatEdge) if value: finalOffset = value else: finalOffset = offset else: finalOffset = offset e1 = Edge.ByOffset2D(flatEdge,finalOffset) newEdges.append(e1) edges = flatEdges newVertices = [] dupVertices = [] if Wire.IsClosed(wire): e1 = newEdges[-1] e2 = newEdges[0] intV = Edge.Intersect2D(e1,e2) if intV: newVertices.append(intV) dupVertices.append(vertices[0]) elif step: edgeVertices= Edge.Vertices(e1) newVertices.append(Vertex.NearestVertex(vertices[-1], Cluster.ByTopologies(edgeVertices), useKDTree=False)) edgeVertices= Edge.Vertices(e2) newVertices.append(Vertex.NearestVertex(vertices[0], Cluster.ByTopologies(edgeVertices), useKDTree=False)) dupVertices.append(vertices[0]) dupVertices.append(vertices[0]) else: tempEdge1 = Edge.ByVertices([Edge.StartVertex(e1), Edge.EndVertex(e2)], tolerance=tolerance, silent=True) normal = Edge.Normal(e1) normal = [normal[0]*finalOffset*10, normal[1]*finalOffset*10, normal[2]*finalOffset*10] tempV = Vertex.ByCoordinates(vertices[0].X()+normal[0], vertices[0].Y()+normal[1], vertices[0].Z()+normal[2]) tempEdge2 = Edge.ByVertices([vertices[0], tempV], tolerance=tolerance, silent=True) intV = Edge.Intersect2D(tempEdge1,tempEdge2) newVertices.append(intV) dupVertices.append(vertices[0]) else: newVertices.append(Edge.StartVertex(newEdges[0])) for i in range(len(newEdges)-1): e1 = newEdges[i] e2 = newEdges[i+1] intV = Edge.Intersect2D(e1,e2) if intV: newVertices.append(intV) dupVertices.append(vertices[i+1]) elif step: newVertices.append(Edge.EndVertex(e1)) newVertices.append(Edge.StartVertex(e2)) dupVertices.append(vertices[i+1]) dupVertices.append(vertices[i+1]) else: tempEdge1 = Edge.ByVertices([Edge.StartVertex(e1), Edge.EndVertex(e2)], tolerance=tolerance, silent=True) normal = Edge.Normal(e1) normal = [normal[0]*finalOffset*10, normal[1]*finalOffset*10, normal[2]*finalOffset*10] tempV = Vertex.ByCoordinates(vertices[i+1].X()+normal[0], vertices[i+1].Y()+normal[1], vertices[i+1].Z()+normal[2]) tempEdge2 = Edge.ByVertices([vertices[i+1], tempV], tolerance=tolerance, silent=True) intV = Edge.Intersect2D(tempEdge1,tempEdge2) newVertices.append(intV) dupVertices.append(vertices[i+1]) vertices = dupVertices if not Wire.IsClosed(wire): newVertices.append(Edge.EndVertex(newEdges[-1])) newWire = Wire.ByVertices(newVertices, close=Wire.IsClosed(wire)) newVertices = Wire.Vertices(newWire) newEdges = Wire.Edges(newWire) miterEdges = [] cleanMiterEdges = [] # Handle miter if miter: for i in range(len(newVertices)): if miterThresholdKey: d = Topology.Dictionary(vertices[i]) value = Dictionary.ValueAtKey(d, key=miterThresholdKey) if value: finalMiterThreshold = value else: finalMiterThreshold = miterThreshold else: finalMiterThreshold = miterThreshold if Vertex.Distance(vertices[i], newVertices[i]) > abs(finalMiterThreshold): st = Topology.SuperTopologies(newVertices[i], newWire, topologyType="edge") if len(st) > 1: e1 = st[0] e2 = st[1] if not Edge.IsCollinear(e1, e2, tolerance=tolerance): e1 = Edge.Reverse(e1, tolerance=tolerance) bisector = Edge.ByVertices([vertices[i], newVertices[i]], tolerance=tolerance) nv = Edge.VertexByDistance(bisector, distance=finalMiterThreshold, origin=Edge.StartVertex(bisector), tolerance=0.0001) vec = Edge.Normal(bisector) nv2 = Topology.Translate(nv, vec[0], vec[1], 0) nv3 = Topology.Translate(nv, -vec[0], -vec[1], 0) miterEdge = Edge.ByVertices([nv2,nv3], tolerance=tolerance) if miterEdge: miterEdge = Edge.SetLength(miterEdge, abs(offset)*10) msv = Edge.Intersect2D(miterEdge, e1) mev = Edge.Intersect2D(miterEdge, e2) if (Vertex.IsInternal(msv, e1,tolerance=0.01) and (Vertex.IsInternal(mev, e2, tolerance=0.01))): miterEdge = Edge.ByVertices([msv, mev], tolerance=tolerance) if miterEdge: cleanMiterEdges.append(miterEdge) miterEdge = Edge.SetLength(miterEdge, Edge.Length(miterEdge)*1.02) miterEdges.append(miterEdge) c = Topology.SelfMerge(Cluster.ByTopologies(newEdges+miterEdges), tolerance=tolerance) vertices = Wire.Vertices(c) subtractEdges = [] for v in vertices: edges = Topology.SuperTopologies(v, c, topologyType="edge") if len(edges) == 2: if not Edge.IsCollinear(edges[0], edges[1], tolerance=tolerance): adjacentVertices = Topology.AdjacentTopologies(v, c) total = 0 for adjV in adjacentVertices: tempEdges = Topology.SuperTopologies(adjV, c, topologyType="edge") total += len(tempEdges) if total == 8: subtractEdges = subtractEdges+edges if len(subtractEdges) > 0: newWire = Topology.Boolean(newWire, Cluster.ByTopologies(subtractEdges), operation="difference", tolerance=tolerance) if len(cleanMiterEdges) > 0: newWire = Topology.Boolean(newWire, Cluster.ByTopologies(cleanMiterEdges), operation="merge", tolerance=tolerance) newWire = Topology.Unflatten(newWire, origin=origin, direction=normal) return newWire @staticmethod def ByVertices(vertices: list, close: bool = True, tolerance: float = 0.0001): """ Creates a wire from the input list of vertices. Parameters ---------- vertices : list the input list of vertices. close : bool , optional If True the last vertex will be connected to the first vertex to close the wire. The default is True. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire. """ from topologicpy.Edge import Edge from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not isinstance(vertices, list): return None vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")] if len(vertexList) < 2: print("Wire.ByVertices - Error: The number of vertices is less than 2. Returning None.") return None edges = [] for i in range(len(vertexList)-1): v1 = vertexList[i] v2 = vertexList[i+1] e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True) if Topology.IsInstance(e, "Edge"): edges.append(e) if close: v1 = vertexList[-1] v2 = vertexList[0] e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True) if Topology.IsInstance(e, "Edge"): edges.append(e) if len(edges) < 1: print("Wire.ByVertices - Error: The number of edges is less than 1. Returning None.") return None elif len(edges) == 1: wire = Wire.ByEdges(edges, orient=False) else: wire = Topology.SelfMerge(Cluster.ByTopologies(edges), tolerance=tolerance) return wire @staticmethod def ByVerticesCluster(cluster, close: bool = True): """ Creates a wire from the input cluster of vertices. Parameters ---------- cluster : topologic_core.cluster the input cluster of vertices. close : bool , optional If True the last vertex will be connected to the first vertex to close the wire. The default is True. Returns ------- topologic_core.Wire The created wire. """ if not Topology.IsInstance(cluster, "Cluster"): return None vertices = [] _ = cluster.Vertices(None, vertices) return Wire.ByVertices(vertices, close) @staticmethod def Circle(origin= None, radius: float = 0.5, sides: int = 16, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a circle. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0). radius : float , optional The radius of the circle. The default is 0.5. sides : int , optional The number of sides of the circle. The default is 16. fromAngle : float , optional The angle in degrees from which to start creating the arc of the circle. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the circle. The default is 360. close : bool , optional If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open. direction : list , optional The vector representing the up direction of the circle. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created circle. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Circle - Error: The input origin parameter is not a valid Vertex. Retruning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Circle - Error: The input placement parameter is not a recognised string. Retruning None.") return None radius = abs(radius) if radius < tolerance: return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: return None baseV = [] xList = [] yList = [] if toAngle < fromAngle: toAngle += 360 if abs(toAngle-fromAngle) < tolerance: return None angleRange = toAngle - fromAngle fromAngle = math.radians(fromAngle) toAngle = math.radians(toAngle) sides = int(math.floor(sides)) for i in range(sides+1): angle = fromAngle + math.radians(angleRange/sides)*i x = math.cos(angle)*radius + origin.X() y = math.sin(angle)*radius + origin.Y() z = origin.Z() xList.append(x) yList.append(y) baseV.append(Vertex.ByCoordinates(x, y, z)) if angleRange == 360: baseWire = Wire.ByVertices(baseV[::-1], close=False) #reversing the list so that the normal points up in Blender else: baseWire = Wire.ByVertices(baseV[::-1], close=close) #reversing the list so that the normal points up in Blender if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, radius, radius, 0) elif placement.lower() == "upperleft": baseWire = Topology.Translate(baseWire, radius, -radius, 0) elif placement.lower() == "lowerright": baseWire = Topology.Translate(baseWire, -radius, radius, 0) elif placement.lower() == "upperright": baseWire = Topology.Translate(baseWire, -radius, -radius, 0) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire @staticmethod def Close(wire, mantissa=6, tolerance=0.0001): """ Closes the input wire Parameters ---------- wire : topologic_core.Wire The input wire. mantissa : int , optional The desired length of the mantissa. The default is 6. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The closed version of the input wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Helper import Helper def nearest_vertex(vertex, vertices): distances = [] for v in vertices: distances.append(Vertex.Distance(vertex, v)) new_vertices = Helper.Sort(vertices, distances) return new_vertices[1] #The first item is the same vertex, so return the next nearest vertex. if not Topology.IsInstance(wire, "Wire"): print("Wire.Close - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if Wire.IsClosed(wire): return wire vertices = Topology.Vertices(wire) ends = [v for v in vertices if Vertex.Degree(v, wire) == 1] if len(ends) < 2: print("Wire.Close - Error: The input wire parameter contains less than two open end vertices. Returning None.") return None geometry = Topology.Geometry(wire, mantissa=mantissa) g_vertices = geometry['vertices'] g_edges = geometry['edges'] used = [] for end in ends: nearest = nearest_vertex(end, ends) if not nearest in used: d = Vertex.Distance(end, nearest) i1 = Vertex.Index(end, vertices) i2 = Vertex.Index(nearest, vertices) if i1 == None or i2 == None: print("Wire.Close - Error: Something went wrong. Returning None.") return None if d < tolerance: g_vertices[i1] = Vertex.Coordinates(end) g_vertices[i2] = Vertex.Coordinates(end) else: if not(([i1, i2] in g_edges) or ([i2, i1] in g_edges)): g_edges.append([i1, i2]) used.append(end) new_wire = Topology.ByGeometry(vertices=g_vertices, edges=g_edges, faces=[], outputMode="wire") return new_wire @staticmethod def ConvexHull(topology, tolerance: float = 0.0001): """ Returns a wire representing the 2D convex hull of the input topology. The vertices of the topology are assumed to be coplanar. Parameters ---------- topology : topologic_core.Topology The input topology. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The convex hull of the input topology. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import sample def Left_index(points): ''' Finding the left most point ''' minn = 0 for i in range(1,len(points)): if points[i][0] < points[minn][0]: minn = i elif points[i][0] == points[minn][0]: if points[i][1] > points[minn][1]: minn = i return minn def orientation(p, q, r): ''' To find orientation of ordered triplet (p, q, r). The function returns following values 0 --> p, q and r are collinear 1 --> Clockwise 2 --> Counterclockwise ''' val = (q[1] - p[1]) * (r[0] - q[0]) - \ (q[0] - p[0]) * (r[1] - q[1]) if val == 0: return 0 elif val > 0: return 1 else: return 2 def convex_hull(points, n): # There must be at least 3 points if n < 3: return # Find the leftmost point l = Left_index(points) hull = [] ''' Start from leftmost point, keep moving counterclockwise until reach the start point again. This loop runs O(h) times where h is number of points in result or output. ''' p = l q = 0 while(True): # Add current point to result hull.append(p) ''' Search for a point 'q' such that orientation(p, q, x) is counterclockwise for all points 'x'. The idea is to keep track of last visited most counterclock- wise point in q. If any point 'i' is more counterclock- wise than q, then update q. ''' q = (p + 1) % n for i in range(n): # If i is more counterclockwise # than current q, then update q if(orientation(points[p], points[i], points[q]) == 2): q = i ''' Now q is the most counterclockwise with respect to p Set p as q for next iteration, so that q is added to result 'hull' ''' p = q # While we don't come to first point if(p == l): break # Print Result return hull f = None # Create a sample face and flatten while not Topology.IsInstance(f, "Face"): vertices = Topology.SubTopologies(topology=topology, subTopologyType="vertex") v = sample(vertices, 3) w = Wire.ByVertices(v) f = Face.ByWire(w, tolerance=tolerance) origin = Topology.Centroid(f) normal = Face.Normal(f) f = Topology.Flatten(f, origin=origin, direction=normal) topology = Topology.Flatten(topology, origin=origin, direction=normal) vertices = Topology.Vertices(topology) points = [] for v in vertices: points.append((Vertex.X(v), Vertex.Y(v))) hull = convex_hull(points, len(points)) hull_vertices = [] for p in hull: hull_vertices.append(Vertex.ByCoordinates(points[p][0], points[p][1], 0)) ch = Wire.ByVertices(hull_vertices) ch = Topology.Unflatten(ch, origin=origin, direction=normal) return ch @staticmethod def Cycles(wire, maxVertices: int = 4, tolerance: float = 0.0001) -> list: """ Returns the closed circuits of wires found within the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. maxVertices : int , optional The maximum number of vertices of the circuits to be searched. The default is 4. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- list The list of circuits (closed wires) found within the input wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge def vIndex(v, vList, tolerance): for i in range(len(vList)): if Vertex.Distance(v, vList[i]) < tolerance: return i+1 return None # rotate cycle path such that it begins with the smallest node def rotate_to_smallest(path): n = path.index(min(path)) return path[n:]+path[:n] def invert(path): return rotate_to_smallest(path[::-1]) def isNew(cycles, path): return not path in cycles def visited(node, path): return node in path def findNewCycles(graph, cycles, path, maxVertices): if len(path) > maxVertices: return start_node = path[0] next_node= None sub = [] #visit each edge and each node of each edge for edge in graph: node1, node2 = edge if start_node in edge: if node1 == start_node: next_node = node2 else: next_node = node1 if not visited(next_node, path): # neighbor node not on path yet sub = [next_node] sub.extend(path) # explore extended path findNewCycles(graph, cycles, sub, maxVertices); elif len(path) > 2 and next_node == path[-1]: # cycle found p = rotate_to_smallest(path); inv = invert(p) if isNew(cycles, p) and isNew(cycles, inv): cycles.append(p) def main(graph, cycles, maxVertices): returnValue = [] for edge in graph: for node in edge: findNewCycles(graph, cycles, [node], maxVertices) for cy in cycles: row = [] for node in cy: row.append(node) returnValue.append(row) return returnValue tEdges = [] _ = wire.Edges(None, tEdges) tVertices = [] _ = wire.Vertices(None, tVertices) tVertices = tVertices graph = [] for anEdge in tEdges: graph.append([vIndex(anEdge.StartVertex(), tVertices, tolerance), vIndex(anEdge.EndVertex(), tVertices, tolerance)]) cycles = [] resultingCycles = main(graph, cycles, maxVertices) result = [] for aRow in resultingCycles: row = [] for anIndex in aRow: row.append(tVertices[anIndex-1]) result.append(row) resultWires = [] for i in range(len(result)): c = result[i] resultEdges = [] for j in range(len(c)-1): v1 = c[j] v2 = c[j+1] e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True) resultEdges.append(e) e = Edge.ByStartVertexEndVertex(c[len(c)-1], c[0], tolerance=tolerance, silent=True) resultEdges.append(e) resultWire = Wire.ByEdges(resultEdges, tolerance=tolerance) resultWires.append(resultWire) return resultWires @staticmethod def Edges(wire) -> list: """ Returns the edges of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- list The list of edges. """ if not Topology.IsInstance(wire, "Wire"): return None edges = [] _ = wire.Edges(None, edges) return edges @staticmethod def Einstein(origin= None, radius: float = 0.5, direction: list = [0, 0, 1], placement: str = "center"): """ Creates an aperiodic monotile, also called an 'einstein' tile (meaning one tile in German, not the name of the famous physist). See https://arxiv.org/abs/2303.10798 Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the tile. The default is None which results in the tiles first vertex being placed at (0, 0, 0). radius : float , optional The radius of the hexagon determining the size of the tile. The default is 0.5. direction : list , optional The vector representing the up direction of the ellipse. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the hexagon determining the location of the tile. This can be "center", or "lowerleft". It is case insensitive. The default is "center". """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology import math def cos(angle): return math.cos(math.radians(angle)) def sin(angle): return math.sin(math.radians(angle)) if not origin: origin = Vertex.ByCoordinates(0, 0, 0) d = cos(30)*radius v1 = Vertex.ByCoordinates(0, 0, 0) v2 = Vertex.ByCoordinates(cos(30)*d, sin(30)*d, 0) v3 = Vertex.ByCoordinates(radius, 0) v4 = Vertex.ByCoordinates(2*radius, 0) v5 = Vertex.ByCoordinates(2*radius+cos(60)*radius*0.5, sin(30)*d, 0) v6 = Vertex.ByCoordinates(1.5*radius, d) v7 = Vertex.ByCoordinates(1.5*radius, 2*d) v8 = Vertex.ByCoordinates(radius, 2*d) v9 = Vertex.ByCoordinates(radius-cos(60)*0.5*radius, 2*d+sin(60)*0.5*radius) v10 = Vertex.ByCoordinates(0, 2*d) v11 = Vertex.ByCoordinates(0, d) v12 = Vertex.ByCoordinates(-radius*0.5, d) v13 = Vertex.ByCoordinates(-cos(30)*d, sin(30)*d, 0) einstein = Wire.ByVertices([v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13], close=True) if placement.lower() == "lowerleft": einstein = Topology.Translate(einstein, radius, d, 0) dx = Vertex.X(origin) dy = Vertex.Y(origin) dz = Vertex.Z(origin) einstein = Topology.Translate(einstein, dx, dy, dz) if direction != [0, 0, 1]: einstein = Topology.Orient(einstein, origin=origin, dirA=[0, 0, 1], dirB=direction) return einstein @staticmethod def Ellipse(origin= None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: float = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates an ellipse and returns all its geometry and parameters. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0). inputMode : int , optional The method by wich the ellipse is defined. The default is 1. Based on the inputMode value, only the following inputs will be considered. The options are: 1. Width and Length (considered inputs: width, length) 2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity) 3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength) 4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength) width : float , optional The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1. length : float , optional The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1. focalLength : float , optional The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3. eccentricity : float , optional The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2. majorAxisLength : float , optional The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4. minorAxisLength : float , optional The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4. sides : int , optional The number of sides of the ellipse. The default is 32. fromAngle : float , optional The angle in degrees from which to start creating the arc of the ellipse. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the ellipse. The default is 360. close : bool , optional If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open. direction : list , optional The vector representing the up direction of the ellipse. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created ellipse """ ellipseAll = Wire.EllipseAll(origin=origin, inputMode=inputMode, width=width, length=length, focalLength=focalLength, eccentricity=eccentricity, majorAxisLength=majorAxisLength, minorAxisLength=minorAxisLength, sides=sides, fromAngle=fromAngle, toAngle=toAngle, close=close, direction=direction, placement=placement, tolerance=tolerance) return ellipseAll["ellipse"] @staticmethod def EllipseAll(origin= None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str ="center", tolerance: float = 0.0001): """ Creates an ellipse and returns all its geometry and parameters. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0). inputMode : int , optional The method by wich the ellipse is defined. The default is 1. Based on the inputMode value, only the following inputs will be considered. The options are: 1. Width and Length (considered inputs: width, length) 2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity) 3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength) 4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength) width : float , optional The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1. length : float , optional The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1. focalLength : float , optional The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3. eccentricity : float , optional The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2. majorAxisLength : float , optional The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4. minorAxisLength : float , optional The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4. sides : int , optional The number of sides of the ellipse. The default is 32. fromAngle : float , optional The angle in degrees from which to start creating the arc of the ellipse. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the ellipse. The default is 360. close : bool , optional If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open. direction : list , optional The vector representing the up direction of the ellipse. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- dictionary A dictionary with the following keys and values: 1. "ellipse" : The ellipse (topologic_core.Wire) 2. "foci" : The two focal points (topologic_core.Cluster containing two vertices) 3. "a" : The major axis length 4. "b" : The minor axis length 5. "c" : The focal length 6. "e" : The eccentricity 7. "width" : The width 8. "length" : The length """ from topologicpy.Vertex import Vertex from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None if inputMode not in [1, 2, 3, 4]: return None if placement.lower() not in ["center", "lowerleft"]: return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: return None width = abs(width) length = abs(length) focalLength= abs(focalLength) eccentricity=abs(eccentricity) majorAxisLength=abs(majorAxisLength) minorAxisLength=abs(minorAxisLength) sides = abs(sides) if width < tolerance or length < tolerance or focalLength < tolerance or eccentricity < tolerance or majorAxisLength < tolerance or minorAxisLength < tolerance or sides < 3: return None if inputMode == 1: w = width l = length a = width/2 b = length/2 c = math.sqrt(abs(b**2 - a**2)) e = c/a elif inputMode == 2: c = focalLength e = eccentricity a = c/e b = math.sqrt(abs(a**2 - c**2)) w = a*2 l = b*2 elif inputMode == 3: c = focalLength b = minorAxisLength a = math.sqrt(abs(b**2 + c**2)) e = c/a w = a*2 l = b*2 elif inputMode == 4: a = majorAxisLength b = minorAxisLength c = math.sqrt(abs(b**2 - a**2)) e = c/a w = a*2 l = b*2 else: return None baseV = [] xList = [] yList = [] if toAngle < fromAngle: toAngle += 360 if abs(toAngle - fromAngle) < tolerance: return None angleRange = toAngle - fromAngle fromAngle = math.radians(fromAngle) toAngle = math.radians(toAngle) sides = int(math.floor(sides)) for i in range(sides+1): angle = fromAngle + math.radians(angleRange/sides)*i x = math.sin(angle)*a + origin.X() y = math.cos(angle)*b + origin.Y() z = origin.Z() xList.append(x) yList.append(y) baseV.append(Vertex.ByCoordinates(x, y, z)) if angleRange == 360: baseWire = Wire.ByVertices(baseV[::-1], close=False) #reversing the list so that the normal points up in Blender else: baseWire = Wire.ByVertices(baseV[::-1], close=close) #reversing the list so that the normal points up in Blender if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, a, b, 0) baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) # Create a Cluster of the two foci v1 = Vertex.ByCoordinates(c+origin.X(), 0+origin.Y(), 0) v2 = Vertex.ByCoordinates(-c+origin.X(), 0+origin.Y(), 0) foci = Cluster.ByTopologies([v1, v2]) if placement.lower() == "lowerleft": foci = Topology.Translate(foci, a, b, 0) foci = Topology.Orient(foci, origin=origin, dirA=[0, 0, 1], dirB=direction) d = {} d['ellipse'] = baseWire d['foci'] = foci d['a'] = a d['b'] = b d['c'] = c d['e'] = e d['w'] = w d['l'] = l return d @staticmethod def EndVertex(wire): """ Returns the end vertex of the input wire. The wire must be manifold and open. """ sv, ev = Wire.StartEndVertices(wire) return ev @staticmethod def ExteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) -> list: """ Returns the exterior angles of the input wire in degrees. The wire must be planar, manifold, and closed. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. mantissa : int , optional The length of the desired mantissa. The default is 6. Returns ------- list The list of exterior angles. """ if not Topology.IsInstance(wire, "Wire"): print("Wire.InteriorAngles - Error: The input wire parameter is not a valid wire. Returning None") return None if not Wire.IsManifold(wire): print("Wire.InteriorAngles - Error: The input wire parameter is non-manifold. Returning None") return None if not Wire.IsClosed(wire): print("Wire.InteriorAngles - Error: The input wire parameter is not closed. Returning None") return None interior_angles = Wire.InteriorAngles(wire, mantissa=mantissa) exterior_angles = [round(360-a, mantissa) for a in interior_angles] return exterior_angles @staticmethod def Fillet(wire, radius: float = 0, radiusKey: str = None, tolerance: float = 0.0001, silent: bool = False): """ Fillets (rounds) the interior and exterior corners of the input wire given the input radius. See https://en.wikipedia.org/wiki/Fillet_(mechanics) Parameters ---------- wire : topologic_core.Wire The input wire. radius : float The desired radius of the fillet. radiusKey : str , optional If specified, the dictionary of the vertices will be queried for this key to specify the desired fillet radius. The default is None. tolerance : float , optional The desired tolerance. 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 ------- topologic_core.Wire The filleted wire. """ def start_from(edge, v): sv = Edge.StartVertex(edge) ev = Edge.EndVertex(edge) if Vertex.Distance(v, ev) < Vertex.Distance(v, sv): return Edge.Reverse(edge) return edge def compute_kite_edges(alpha, r): # Convert angle to radians alpha = math.radians(alpha) *0.5 h = r/math.cos(alpha) a = math.sqrt(h*h - r*r) return [a,h] import math from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Vector import Vector from topologicpy.Dictionary import Dictionary if not Topology.IsInstance(wire, "Wire"): if not silent: print("Wire.Fillet - Error: The input wire parameter is not a valid wire. Returning None.") return None if not Wire.IsManifold(wire): if not silent: print("Wire.Fillet - Error: The input wire parameter is not manifold. Returning None.") return None if not Topology.IsPlanar(wire): if not silent: print("Wire.Fillet - Error: The input wire parameter is not planar. Returning None.") return None orig_radius = radius f = Face.BoundingRectangle(wire, tolerance=tolerance) normal = Face.Normal(f) flat_wire = Topology.Flatten(wire, origin=Vertex.Origin(), direction=normal) vertices = Topology.Vertices(flat_wire) final_vertices = [] fillets = [] for v in vertices: radius = orig_radius edges = Topology.SuperTopologies(v, flat_wire, topologyType="edge") if len(edges) == 2: for edge in edges: ev = Edge.EndVertex(edge) if Vertex.Distance(v, ev) < tolerance: edge0 = edge else: edge1 = edge ang = Edge.Angle(edge0, edge1) e1 = start_from(edge0, v) e2 = start_from(edge1, v) dir1 = Edge.Direction(e1) dir2 = Edge.Direction(e2) if Vector.IsParallel(dir1, dir2) or Vector.IsAntiParallel(dir1, dir2): pass else: if isinstance(radiusKey, str): d = Topology.Dictionary(v) if Topology.IsInstance(d, "Dictionary"): v_radius = Dictionary.ValueAtKey(d, radiusKey) if isinstance(v_radius, float) or isinstance(v_radius, int): if v_radius >= 0: radius = v_radius if radius > 0: dir_bisector = Vector.Bisect(dir1,dir2) a, h = compute_kite_edges(ang, radius) if a <= Edge.Length(e1) and a <= Edge.Length(e2): v1 = Topology.TranslateByDirectionDistance(v, dir1, a) center = Topology.TranslateByDirectionDistance(v, dir_bisector, h) v2 = Topology.TranslateByDirectionDistance(v, dir2, a) r1 = Edge.ByVertices(center, v1) dir1 = Edge.Direction(r1) r2 = Edge.ByVertices(center, v2) dir2 = Edge.Direction(r2) compass1 = Vector.CompassAngle(Vector.East(), dir1)*-1 compass2 = Vector.CompassAngle(Vector.East(), dir2)*-1 if compass2 < compass1: temp = compass2 compass2 = compass1 compass1 = temp w1 = Wire.Circle(origin=center, radius=radius, fromAngle=compass1, toAngle=compass2, close=False) w2 = Wire.Circle(origin=center, radius=radius, fromAngle=compass2, toAngle=compass1, close=False) if Wire.Length(w1) < Wire.Length(w2): fillet = w1 else: fillet = w2 f_sv = Wire.StartVertex(fillet) if Vertex.Distance(f_sv, edge1) < Vertex.Distance(f_sv, edge0): fillet = Wire.Reverse(fillet) final_vertices += Topology.Vertices(fillet) else: if not silent: print("Wire.Fillet - Error: The specified fillet radius is too large to be applied. Skipping.") else: final_vertices.append(v) else: final_vertices.append(v) flat_wire = Wire.ByVertices(final_vertices, close=Wire.IsClosed(wire)) # Unflatten the wire return_wire = Topology.Unflatten(flat_wire, origin=Vertex.Origin(), direction=normal) return return_wire @staticmethod def InteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) -> list: """ Returns the interior angles of the input wire in degrees. The wire must be planar, manifold, and closed. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- list The list of interior angles. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Topology import Topology from topologicpy.Vector import Vector from topologicpy.Dictionary import Dictionary if not Topology.IsInstance(wire, "Wire"): print("Wire.InteriorAngles - Error: The input wire parameter is not a valid wire. Returning None") return None if not Wire.IsManifold(wire): print("Wire.InteriorAngles - Error: The input wire parameter is non-manifold. Returning None") return None if not Wire.IsClosed(wire): print("Wire.InteriorAngles - Error: The input wire parameter is not closed. Returning None") return None f = Face.ByWire(wire) normal = Face.Normal(f) origin = Topology.Centroid(f) w = Topology.Flatten(wire, origin=origin, direction=normal) angles = [] edges = Topology.Edges(w) for i in range(len(edges)-1): e1 = edges[i] e2 = edges[i+1] a = round(360 - Vector.CompassAngle(Edge.Direction(e1), Edge.Direction(e2)), mantissa) angles.append(a) e1 = edges[len(edges)-1] e2 = edges[0] a = round(360 - Vector.CompassAngle(Edge.Direction(e1), Edge.Direction(e2)), mantissa) angles = [a]+angles return angles @staticmethod def Interpolate(wires: list, n: int = 5, outputType: str = "default", mapping: str = "default", tolerance: float = 0.0001): """ Creates *n* number of wires that interpolate between wireA and wireB. Parameters ---------- wireA : topologic_core.Wire The first input wire. wireB : topologic_core.Wire The second input wire. n : int , optional The number of intermediate wires to create. The default is 5. outputType : str , optional The desired type of output. The options are case insensitive. The default is "contour". The options are: - "Default" or "Contours" (wires are not connected) - "Raster or "Zigzag" or "Toolpath" (the wire ends are connected to create a continous path) - "Grid" (the wire ends are connected to create a grid). mapping : str , optional The desired type of mapping for wires with different number of vertices. It is case insensitive. The default is "default". The options are: - "Default" or "Repeat" which repeats the last vertex of the wire with the least number of vertices - "Nearest" which maps the vertices of one wire to the nearest vertex of the next wire creating a list of equal number of vertices. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- tTopology The created interpolated wires as well as the input wires. The return type can be a topologic_core.Cluster or a topologic_core.Wire based on options. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Helper import Helper outputType = outputType.lower() if outputType not in ["default", "contours", "raster", "zigzag", "toolpath", "grid"]: return None if outputType == "default" or outputType == "contours": outputType = "contours" if outputType == "raster" or outputType == "zigzag" or outputType == "toolpath": outputType = "zigzag" mapping = mapping.lower() if mapping not in ["default", "nearest", "repeat"]: print("Wire.Interpolate - Error: The mapping input parameter is not recognized. Returning None.") return None def nearestVertex(v, vertices): distances = [Vertex.Distance(v, vertex) for vertex in vertices] return vertices[distances.index(sorted(distances)[0])] def replicate(vertices, mapping="default"): vertices = Helper.Repeat(vertices) finalList = vertices if mapping == "nearest": finalList = [vertices[0]] for i in range(len(vertices)-1): loopA = vertices[i] loopB = vertices[i+1] nearestVertices = [] for j in range(len(loopA)): nv = nearestVertex(loopA[j], loopB) nearestVertices.append(nv) finalList.append(nearestVertices) return finalList def process(verticesA, verticesB, n=5): contours = [verticesA] for i in range(1, n+1): u = float(i)/float(n+1) temp_vertices = [] for j in range(len(verticesA)): temp_v = Edge.VertexByParameter(Edge.ByVertices([verticesA[j], verticesB[j]], tolerance=tolerance), u) temp_vertices.append(temp_v) contours.append(temp_vertices) return contours if len(wires) < 2: return None vertices = [] for wire in wires: vertices.append(Topology.SubTopologies(wire, subTopologyType="vertex")) vertices = replicate(vertices, mapping=mapping) contours = [] finalWires = [] for i in range(len(vertices)-1): verticesA = vertices[i] verticesB = vertices[i+1] contour = process(verticesA=verticesA, verticesB=verticesB, n=n) contours += contour for c in contour: finalWires.append(Wire.ByVertices(c, Wire.IsClosed(wires[i]))) contours.append(vertices[-1]) finalWires.append(wires[-1]) ridges = [] if outputType == "grid" or outputType == "zigzag": for i in range(len(contours)-1): verticesA = contours[i] verticesB = contours[i+1] if outputType == "grid": for j in range(len(verticesA)): ridges.append(Edge.ByVertices([verticesA[j], verticesB[j]], tolerance=tolerance)) elif outputType == "zigzag": if i%2 == 0: sv = verticesA[-1] ev = verticesB[-1] ridges.append(Edge.ByVertices([sv, ev], tolerance=tolerance)) else: sv = verticesA[0] ev = verticesB[0] ridges.append(Edge.ByVertices([sv, ev], tolerance=tolerance)) return Topology.SelfMerge(Cluster.ByTopologies(finalWires+ridges), tolerance=tolerance) @staticmethod def Invert(wire): """ Creates a wire that is an inverse (mirror) of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- topologic_core.Wire The inverted wire. """ if not Topology.IsInstance(wire, "Wire"): return None vertices = Wire.Vertices(wire) reversed_vertices = vertices[::-1] return Wire.ByVertices(reversed_vertices) @staticmethod def IsClosed(wire) -> bool: """ Returns True if the input wire is closed. Returns False otherwise. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- bool True if the input wire is closed. False otherwise. """ status = None if wire: if Topology.IsInstance(wire, "Wire"): status = wire.IsClosed() return status @staticmethod def IsManifold(wire) -> bool: """ Returns True if the input wire is manifold. Returns False otherwise. A manifold wire is one where its vertices have a degree of 1 or 2. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- bool True if the input wire is manifold. False otherwise. """ from topologicpy.Vertex import Vertex if not Topology.IsInstance(wire, "Wire"): print("Wire.IsManifold - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None vertices = Wire.Vertices(wire) for v in vertices: if Vertex.Degree(v, hostTopology=wire) > 2: return False return True @staticmethod def IsSimilar(wireA, wireB, angTolerance: float = 0.1, tolerance: float = 0.0001) -> bool: """ Returns True if the input wires are similar. Returns False otherwise. The wires must be closed. Parameters ---------- wireA : topologic_core.Wire The first input wire. wireB : topologic_core.Wire The second input wire. angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- bool True if the two input wires are similar. False otherwise. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge def isCyclicallyEquivalent(u, v, lengthTolerance, angleTolerance): n, i, j = len(u), 0, 0 if n != len(v): return False while i < n and j < n: if (i % 2) == 0: tol = lengthTolerance else: tol = angleTolerance k = 1 while k <= n and math.fabs(u[(i + k) % n]- v[(j + k) % n]) <= tol: k += 1 if k > n: return True if math.fabs(u[(i + k) % n]- v[(j + k) % n]) > tol: i += k else: j += k return False def angleBetweenEdges(e1, e2, tolerance): a = e1.EndVertex().X() - e1.StartVertex().X() b = e1.EndVertex().Y() - e1.StartVertex().Y() c = e1.EndVertex().Z() - e1.StartVertex().Z() d = Vertex.Distance(e1.EndVertex(), e2.StartVertex()) if d <= tolerance: d = e2.StartVertex().X() - e2.EndVertex().X() e = e2.StartVertex().Y() - e2.EndVertex().Y() f = e2.StartVertex().Z() - e2.EndVertex().Z() else: d = e2.EndVertex().X() - e2.StartVertex().X() e = e2.EndVertex().Y() - e2.StartVertex().Y() f = e2.EndVertex().Z() - e2.StartVertex().Z() dotProduct = a*d + b*e + c*f modOfVector1 = math.sqrt( a*a + b*b + c*c)*math.sqrt(d*d + e*e + f*f) angle = dotProduct/modOfVector1 angleInDegrees = math.degrees(math.acos(angle)) return angleInDegrees def getInteriorAngles(edges, tolerance): angles = [] for i in range(len(edges)-1): e1 = edges[i] e2 = edges[i+1] angles.append(angleBetweenEdges(e1, e2, tolerance)) return angles def getRep(edges, tolerance): angles = getInteriorAngles(edges, tolerance) lengths = [] for anEdge in edges: lengths.append(Edge.Length(anEdge)) minLength = min(lengths) normalisedLengths = [] for aLength in lengths: normalisedLengths.append(aLength/minLength) return [x for x in itertools.chain(*itertools.zip_longest(normalisedLengths, angles)) if x is not None] if (wireA.IsClosed() == False): return None if (wireB.IsClosed() == False): return None edgesA = [] _ = wireA.Edges(None, edgesA) edgesB = [] _ = wireB.Edges(None, edgesB) if len(edgesA) != len(edgesB): return False repA = getRep(list(edgesA), tolerance) repB = getRep(list(edgesB), tolerance) if isCyclicallyEquivalent(repA, repB, tolerance, angTolerance): return True if isCyclicallyEquivalent(repA, repB[::-1], tolerance, angTolerance): return True return False @staticmethod def Length(wire, mantissa: int = 6) -> float: """ Returns the length of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- float The length of the input wire. Test """ from topologicpy.Edge import Edge if not wire: return None if not Topology.IsInstance(wire, "Wire"): return None totalLength = None try: edges = [] _ = wire.Edges(None, edges) totalLength = 0 for anEdge in edges: totalLength = totalLength + Edge.Length(anEdge) totalLength = round(totalLength, mantissa) except: totalLength = None return totalLength @staticmethod def Line(origin= None, length: float = 1, direction: list = [1, 0, 0], sides: int = 2, placement: str ="center"): """ Creates a straight line wire using the input parameters. Parameters ---------- origin : topologic_core.Vertex , optional The origin location of the box. The default is None which results in the edge being placed at (0, 0, 0). length : float , optional The desired length of the edge. The default is 1.0. direction : list , optional The desired direction (vector) of the edge. The default is [1, 0, 0] (along the X-axis). sides : int , optional The desired number of sides/segments. The minimum number of sides is 2. The default is 2. placement : str , optional The desired placement of the edge. The options are: 1. "center" which places the center of the edge at the origin. 2. "start" which places the start of the edge at the origin. 3. "end" which places the end of the edge at the origin. The default is "center". Returns ------- topologic_core.Edge The created edge """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Vector import Vector from topologicpy.Topology import Topology if origin == None: origin = Vertex.Origin() if not Topology.IsInstance(origin, "Vertex"): print("Wire.Line - Error: The input origin is not a valid vertex. Returning None.") return None if length <= 0: print("Wire.Line - Error: The input length is less than or equal to zero. Returning None.") return None if not isinstance(direction, list): print("Wire.Line - Error: The input direction is not a valid list. Returning None.") return None if not len(direction) == 3: print("Wire.Line - Error: The length of the input direction is not equal to three. Returning None.") return None if sides < 2: print("Wire.Line - Error: The number of sides cannot be less than two. Consider using Edge.Line() instead. Returning None.") return None edge = Edge.Line(origin=origin, length=length, direction=direction, placement=placement) vertices = [Edge.StartVertex(edge)] unitDistance = float(1)/float(sides) for i in range(1, sides): vertices.append(Edge.VertexByParameter(edge, i*unitDistance)) vertices.append(Edge.EndVertex(edge)) return Wire.ByVertices(vertices) @staticmethod def OrientEdges(wire, vertexA, tolerance=0.0001): """ Returns a correctly oriented head-to-tail version of the input wire. The input wire must be manifold. Parameters ---------- wire : topologic_core.Wire The input wire. vertexA : topologic_core.Vertex The desired start vertex of the wire. tolerance : float, optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The oriented wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge if not Topology.IsInstance(wire, "Wire"): print("Wire.OrientEdges - Error: The input wire parameter is not a valid wire. Returning None.") return None if not Topology.IsInstance(vertexA, "Vertex"): print("Wire.OrientEdges - Error: The input vertexA parameter is not a valid vertex. Returning None.") return None if not Wire.IsManifold(wire): print("Wire.OrientEdges - Error: The input wire parameter is not a manifold wire. Returning None.") return None oriented_edges = [] remaining_edges = Topology.Edges(wire) current_vertex = vertexA while remaining_edges: next_edge = None for edge in remaining_edges: if Vertex.Distance(Edge.StartVertex(edge), current_vertex) < tolerance: next_edge = edge break elif Vertex.Distance(Edge.EndVertex(edge), current_vertex) < tolerance: next_edge = Edge.Reverse(edge) break if next_edge: oriented_edges.append(next_edge) remaining_edges.remove(next_edge) current_vertex = Edge.EndVertex(next_edge) else: # Unable to find a next edge connected to the current vertex break vertices = [Edge.StartVertex(oriented_edges[0])] for i, edge in enumerate(oriented_edges): vertices.append(Edge.EndVertex(edge)) return Wire.ByVertices(vertices, close=Wire.IsClosed(wire)) @staticmethod def Planarize(wire, origin= None, mantissa: int = 6, tolerance: float = 0.0001): """ Returns a planarized version of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float, optional The desired tolerance. The default is 0.0001. origin : topologic_core.Vertex , optional The desired origin of the plane unto which the planar wire will be projected. If set to None, the centroid of the input wire will be chosen. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- topologic_core.Wire The planarized wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(wire, "Wire"): print("Wire.Planarize - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if origin == None: origin = Vertex.Origin() if not Topology.IsInstance(origin, "Vertex"): print("Wire.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None vertices = Topology.Vertices(wire) edges = Topology.Edges(wire) plane_equation = Vertex.PlaneEquation(vertices, mantissa=mantissa) rect = Face.RectangleByPlaneEquation(origin=origin , equation=plane_equation, tolerance=tolerance) new_vertices = [Vertex.Project(v, rect, mantissa=mantissa) for v in vertices] new_vertices = Vertex.Fuse(new_vertices, mantissa=mantissa, tolerance=tolerance) new_edges = [] for edge in edges: sv = Edge.StartVertex(edge) ev = Edge.EndVertex(edge) sv1 = Vertex.Project(sv, rect) i = Vertex.Index(sv1, new_vertices, tolerance=tolerance) if i: sv1 = new_vertices[i] ev1 = Vertex.Project(ev, rect) i = Vertex.Index(ev1, new_vertices, tolerance=tolerance) if i: ev1 = new_vertices[i] new_edges.append(Edge.ByVertices([sv1, ev1])) return Topology.SelfMerge(Cluster.ByTopologies(new_edges), tolerance=tolerance) @staticmethod def Project(wire, face, direction: list = None, mantissa: int = 6, tolerance: float = 0.0001): """ Creates a projection of the input wire unto the input face. Parameters ---------- wire : topologic_core.Wire The input wire. face : topologic_core.Face The face unto which to project the input wire. direction : list, optional The vector representing the direction of the projection. If None, the reverse vector of the receiving face normal will be used. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The projected wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Topology import Topology if not wire: return None if not Topology.IsInstance(wire, "Wire"): return None if not face: return None if not Topology.IsInstance(face, "Face"): return None if not direction: direction = -1*Face.NormalAtParameters(face, 0.5, 0.5, "XYZ", mantissa) large_face = Topology.Scale(face, face.CenterOfMass(), 500, 500, 500) edges = [] _ = wire.Edges(None, edges) projected_edges = [] if large_face: if (Topology.Type(large_face) == Topology.TypeID("Face")): for edge in edges: if edge: if (Topology.Type(edge) == Topology.TypeID("Edge")): sv = edge.StartVertex() ev = edge.EndVertex() psv = Vertex.Project(vertex=sv, face=large_face, direction=direction) pev = Vertex.Project(vertex=ev, face=large_face, direction=direction) if psv and pev: try: pe = Edge.ByVertices([psv, pev], tolerance=tolerance) projected_edges.append(pe) except: continue w = Wire.ByEdges(projected_edges, tolerance=tolerance) return w @staticmethod def Rectangle(origin= None, width: float = 1.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates a rectangle. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0). width : float , optional The width of the rectangle. The default is 1.0. length : float , optional The length of the rectangle. The default is 1.0. direction : list , optional The vector representing the up direction of the rectangle. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the rectangle. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center". angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created rectangle. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Rectangle - Error: specified origin is not a topologic vertex. Retruning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Rectangle - Error: Could not find placement in the list of placements. Retruning None.") return None width = abs(width) length = abs(length) if width < tolerance or length < tolerance: print("Wire.Rectangle - Error: One or more of the specified dimensions is below the tolerance value. Retruning None.") return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: print("Wire.Rectangle - Error: The direction vector magnitude is below the tolerance value. Retruning None.") return None xOffset = 0 yOffset = 0 if placement.lower() == "lowerleft": xOffset = width*0.5 yOffset = length*0.5 elif placement.lower() == "upperleft": xOffset = width*0.5 yOffset = -length*0.5 elif placement.lower() == "lowerright": xOffset = -width*0.5 yOffset = length*0.5 elif placement.lower() == "upperright": xOffset = -width*0.5 yOffset = -length*0.5 vb1 = Vertex.ByCoordinates(origin.X()-width*0.5+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb2 = Vertex.ByCoordinates(origin.X()+width*0.5+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb3 = Vertex.ByCoordinates(origin.X()+width*0.5+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) vb4 = Vertex.ByCoordinates(origin.X()-width*0.5+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], True) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire @staticmethod def RemoveCollinearEdges(wire, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Removes any collinear edges in the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire without any collinear edges. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Wire import Wire from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology def cleanup(wire, tolerance): vertices = Topology.Vertices(wire) vertices = Vertex.Fuse(vertices, tolerance=tolerance) edges = Topology.Edges(wire) new_edges = [] for edge in edges: sv = Edge.StartVertex(edge) sv = vertices[Vertex.Index(sv, vertices)] ev = Edge.EndVertex(edge) ev = vertices[Vertex.Index(ev, vertices)] new_edges.append(Edge.ByVertices([sv,ev])) new_wire = Topology.SelfMerge(Cluster.ByTopologies(new_edges), tolerance=tolerance) return new_wire def rce(wire, angTolerance=0.1): if not Topology.IsInstance(wire, "Wire"): return wire final_wire = None vertices = [] wire_verts = [] try: _ = wire.Vertices(None, vertices) except: return wire for aVertex in vertices: edges = [] _ = aVertex.Edges(wire, edges) if len(edges) > 1: if not Edge.IsCollinear(edges[0], edges[1], tolerance=tolerance): wire_verts.append(aVertex) else: wire_verts.append(aVertex) if len(wire_verts) > 2: if wire.IsClosed(): final_wire = Wire.ByVertices(wire_verts, close=True) else: final_wire = Wire.ByVertices(wire_verts, close=False) elif len(wire_verts) == 2: final_wire = Edge.ByStartVertexEndVertex(wire_verts[0], wire_verts[1], tolerance=tolerance, silent=True) return final_wire new_wire = cleanup(wire, tolerance=tolerance) if not Wire.IsManifold(new_wire): wires = Wire.Split(new_wire) else: wires = [new_wire] returnWires = [] for aWire in wires: if not Topology.IsInstance(aWire, "Wire"): returnWires.append(aWire) else: returnWires.append(rce(aWire, angTolerance=angTolerance)) if len(returnWires) == 1: returnWire = returnWires[0] if Topology.IsInstance(returnWire, "Edge"): return Wire.ByEdges([returnWire], tolerance=tolerance) elif Topology.IsInstance(returnWire, "Wire"): return returnWire else: return wire elif len(returnWires) > 1: returnWire = Topology.SelfMerge(Cluster.ByTopologies(returnWires)) if Topology.IsInstance(returnWire, "Edge"): return Wire.ByEdges([returnWire], tolerance=tolerance) elif Topology.IsInstance(returnWire, "Wire"): return returnWire else: return wire else: return wire @staticmethod def Reverse(wire, tolerance: float = 0.0001): """ Creates a wire that has the reverse direction of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The reversed wire. """ from topologicpy.Topology import Topology if not Topology.IsInstance(wire, "Wire"): print("Wire.Reverse - Error: The input wire parameter is not a valid wire. Returning None.") return None if not Wire.IsManifold(wire): print("Wire.Reverse - Error: The input wire parameter is not a manifold wire. Returning None.") return None vertices = Topology.Vertices(wire) vertices.reverse() new_wire = Wire.ByVertices(vertices, close=Wire.IsClosed(wire), tolerance=tolerance) return new_wire @staticmethod def Roof(face, angle: float = 45, tolerance: float = 0.001): """ Creates a hipped roof through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com> This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel Parameters ---------- face : topologic_core.Face The input face. angle : float , optioal The desired angle in degrees of the roof. The default is 45. tolerance : float , optional The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better) Returns ------- topologic_core.Wire The created roof. This method returns the roof as a set of edges. No faces are created. """ from topologicpy import Polyskel from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from topologicpy.Helper import Helper import topologic_core as topologic import math def subtrees_to_edges(subtrees, polygon, slope): polygon_z = {} for x, y, z in polygon: polygon_z[(x, y)] = z edges = [] for subtree in subtrees: source = subtree.source height = subtree.height z = slope * height source_vertex = Vertex.ByCoordinates(source.x, source.y, z) for sink in subtree.sinks: if (sink.x, sink.y) in polygon_z: z = 0 else: z = None for st in subtrees: if st.source.x == sink.x and st.source.y == sink.y: z = slope * st.height break for sk in st.sinks: if sk.x == sink.x and sk.y == sink.y: z = slope * st.height break if z is None: height = subtree.height z = slope * height sink_vertex = Vertex.ByCoordinates(sink.x, sink.y, z) if (source.x, source.y) == (sink.x, sink.y): continue e = Edge.ByStartVertexEndVertex(source_vertex, sink_vertex, tolerance=tolerance, silent=True) if e not in edges and e != None: edges.append(e) return edges def face_to_skeleton(face, angle=0): normal = Face.Normal(face) eb_wire = Face.ExternalBoundary(face) ib_wires = Face.InternalBoundaries(face) eb_vertices = Topology.Vertices(eb_wire) if normal[2] > 0: eb_vertices = list(reversed(eb_vertices)) eb_polygon_coordinates = [(v.X(), v.Y(), v.Z()) for v in eb_vertices] eb_polygonxy = [(x[0], x[1]) for x in eb_polygon_coordinates] ib_polygonsxy = [] zero_coordinates = eb_polygon_coordinates for ib_wire in ib_wires: ib_vertices = Topology.Vertices(ib_wire) if normal[2] > 0: ib_vertices = list(reversed(ib_vertices)) ib_polygon_coordinates = [(v.X(), v.Y(), v.Z()) for v in ib_vertices] ib_polygonxy = [(x[0], x[1]) for x in ib_polygon_coordinates] ib_polygonsxy.append(ib_polygonxy) zero_coordinates += ib_polygon_coordinates skeleton = Polyskel.skeletonize(eb_polygonxy, ib_polygonsxy) slope = math.tan(math.radians(angle)) roofEdges = subtrees_to_edges(skeleton, zero_coordinates, slope) roofEdges = Helper.Flatten(roofEdges)+Topology.Edges(face) roofTopology = Topology.SelfMerge(Cluster.ByTopologies(roofEdges), tolerance=tolerance) return roofTopology if not Topology.IsInstance(face, "Face"): return None angle = abs(angle) if angle >= 90-tolerance: return None origin = Topology.Centroid(face) normal = Face.Normal(face) flat_face = Topology.Flatten(face, origin=origin, direction=normal) d = Topology.Dictionary(flat_face) roof = face_to_skeleton(flat_face, angle) if not roof: return None roof = Topology.Unflatten(roof, origin=origin, direction=normal) return roof @staticmethod def Simplify(wire, tolerance=0.0001): """ Simplifies the input wire edges based on the Douglas Peucker algorthim. See https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm Part of this code was contributed by gaoxipeng. See https://github.com/wassimj/topologicpy/issues/35 Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed. Returns ------- topologic_core.Wire The simplified wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology def perpendicular_distance(point, line_start, line_end): # Calculate the perpendicular distance from a point to a line segment x0 = point.X() y0 = point.Y() x1 = line_start.X() y1 = line_start.Y() x2 = line_end.X() y2 = line_end.Y() numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) denominator = Vertex.Distance(line_start, line_end) return numerator / denominator def douglas_peucker(wire, tolerance): if isinstance(wire, list): points = wire else: points = Wire.Vertices(wire) # points.insert(0, points.pop()) if len(points) <= 2: return points # Use the first and last points in the list as the starting and ending points start_point = points[0] end_point = points[-1] # Find the point with the maximum distance max_distance = 0 max_index = 0 for i in range(1, len(points) - 1): d = perpendicular_distance(points[i], start_point, end_point) if d > max_distance: max_distance = d max_index = i # If the maximum distance is less than the tolerance, no further simplification is needed if max_distance <= tolerance: return [start_point, end_point] # Recursively simplify first_segment = douglas_peucker(points[:max_index + 1], tolerance) second_segment = douglas_peucker(points[max_index:], tolerance) # Merge the two simplified segments return first_segment[:-1] + second_segment if not Topology.IsInstance(wire, "Wire"): print("Wire.Simplify = Error: The input wire parameter is not a Wire. Returning None.") return None if not Wire.IsManifold(wire): wires = Wire.Split(wire) new_wires = [] for w in wires: if Topology.IsInstance(w, "Edge"): if Edge.Length(w) > tolerance: new_wires.append(w) elif Topology.IsInstance(w, "Wire"): new_wires.append(Wire.Simplify(w, tolerance=tolerance)) return_wire = Topology.SelfMerge(Cluster.ByTopologies(new_wires)) return return_wire new_vertices = douglas_peucker(wire, tolerance=tolerance) new_wire = Wire.ByVertices(new_vertices, close=Wire.IsClosed(wire)) return new_wire @staticmethod def Skeleton(face, tolerance=0.001): """ Creates a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com> This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel Parameters ---------- face : topologic_core.Face The input face. tolerance : float , optional The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better) Returns ------- topologic_core.Wire The created straight skeleton. """ if not Topology.IsInstance(face, "Face"): return None return Wire.Roof(face, angle=0, tolerance=tolerance) @staticmethod def Spiral(origin = None, radiusA : float = 0.05, radiusB : float = 0.5, height : float = 1, turns : int = 10, sides : int = 36, clockwise : bool = False, reverse : bool = False, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a spiral. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the spiral. The default is None which results in the spiral being placed at (0, 0, 0). radiusA : float , optional The initial radius of the spiral. The default is 0.05. radiusB : float , optional The final radius of the spiral. The default is 0.5. height : float , optional The height of the spiral. The default is 1. turns : int , optional The number of turns of the spiral. The default is 10. sides : int , optional The number of sides of one full turn in the spiral. The default is 36. clockwise : bool , optional If set to True, the spiral will be oriented in a clockwise fashion. Otherwise, it will be oriented in an anti-clockwise fashion. The default is False. reverse : bool , optional If set to True, the spiral will increase in height from the center to the circumference. Otherwise, it will increase in height from the conference to the center. The default is False. direction : list , optional The vector representing the up direction of the spiral. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the spiral. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center". Returns ------- topologic_core.Wire The created spiral. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology import math if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Spiral - Error: the input origin is not a valid topologic Vertex. Returning None.") return None if radiusA <= 0: print("Wire.Spiral - Error: the input radiusA cannot be less than or equal to zero. Returning None.") return None if radiusB <= 0: print("Wire.Spiral - Error: the input radiusB cannot be less than or equal to zero. Returning None.") return None if radiusA == radiusB: print("Wire.Spiral - Error: the inputs radiusA and radiusB cannot be equal. Returning None.") return None if radiusB > radiusA: temp = radiusA radiusA = radiusB radiusB = temp if turns <= 0: print("Wire.Spiral - Error: the input turns cannot be less than or equal to zero. Returning None.") return None if sides < 3: print("Wire.Spiral - Error: the input sides cannot be less than three. Returning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Spiral - Error: the input placement string is not one of center, lowerleft, upperleft, lowerright, or upperright. Returning None.") return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: print("Wire.Spiral - Error: the input direction vector is not a valid direction. Returning None.") return None vertices = [] xList = [] yList = [] zList = [] if clockwise: cw = -1 else: cw = 1 n_vertices = sides*turns + 1 zOffset = height/float(n_vertices) if reverse == True: z = height else: z = 0 ang = 0 angOffset = float(360/float(sides)) b = (radiusB - radiusA)/(2*math.pi*turns) while ang <= 360*turns: rad = math.radians(ang) x = (radiusA + b*rad)*math.cos(rad)*cw xList.append(x) y = (radiusA + b*rad)*math.sin(rad) yList.append(y) zList.append(z) if reverse == True: z = z - zOffset else: z = z + zOffset vertices.append(Vertex.ByCoordinates(x, y, z)) ang = ang + angOffset minX = min(xList) maxX = max(xList) minY = min(yList) maxY = max(yList) radius = radiusA + radiusB*turns*0.5 baseWire = Wire.ByVertices(vertices, close=False) if placement.lower() == "center": baseWire = Topology.Translate(baseWire, 0, 0, -height*0.5) if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, -minX, -minY, 0) elif placement.lower() == "upperleft": baseWire = Topology.Translate(baseWire, -minX, -maxY, 0) elif placement.lower() == "lowerright": baseWire = Topology.Translate(baseWire, -maxX, -minY, 0) elif placement.lower() == "upperright": baseWire = Topology.Translate(baseWire, -maxX, -maxY, 0) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire @staticmethod def Split(wire) -> list: """ Splits the input wire into segments at its intersections (i.e. at any vertex where more than two edges meet). Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- list The list of split wire segments. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology def vertexDegree(v, wire): edges = [] _ = v.Edges(wire, edges) return len(edges) def vertexOtherEdge(vertex, edge, wire): edges = [] _ = vertex.Edges(wire, edges) if Topology.IsSame(edges[0], edge): return edges[-1] else: return edges[0] def edgeOtherVertex(edge, vertex): vertices = [] _ = edge.Vertices(None, vertices) if Topology.IsSame(vertex, vertices[0]): return vertices[-1] else: return vertices[0] def edgeInList(edge, edgeList): for anEdge in edgeList: if Topology.IsSame(anEdge, edge): return True return False vertices = [] _ = wire.Vertices(None, vertices) hubs = [] for aVertex in vertices: if vertexDegree(aVertex, wire) > 2: hubs.append(aVertex) wires = [] global_edges = [] for aVertex in hubs: hub_edges = [] _ = aVertex.Edges(wire, hub_edges) wire_edges = [] for hub_edge in hub_edges: if not edgeInList(hub_edge, global_edges): current_edge = hub_edge oe = edgeOtherVertex(current_edge, aVertex) while vertexDegree(oe, wire) == 2: if not edgeInList(current_edge, global_edges): global_edges.append(current_edge) wire_edges.append(current_edge) current_edge = vertexOtherEdge(oe, current_edge, wire) oe = edgeOtherVertex(current_edge, oe) if not edgeInList(current_edge, global_edges): global_edges.append(current_edge) wire_edges.append(current_edge) if len(wire_edges) > 1: wires.append(Cluster.ByTopologies(wire_edges).SelfMerge()) else: wires.append(wire_edges[0]) wire_edges = [] if len(wires) < 1: return [wire] return wires @staticmethod def Square(origin= None, size: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a square. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0). size : float , optional The size of the square. The default is 1.0. direction : list , optional The vector representing the up direction of the square. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the square. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created square. """ return Wire.Rectangle(origin=origin, width=size, length=size, direction=direction, placement=placement, tolerance=tolerance) @staticmethod def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.0, b: float = 2.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates a Squircle which is a hybrid between a circle and a square. See https://en.wikipedia.org/wiki/Squircle Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the squircle. The default is None which results in the squircle being placed at (0, 0, 0). radius : float , optional The radius of the squircle. The default is 0.5. sides : int , optional The number of sides of the squircle. The default is 121. a : float , optional The "a" factor affects the x position of the points to interpolate between a circle and a square. A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0. b : float , optional The "b" factor affects the y position of the points to interpolate between a circle and a square. A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0. radius : float , optional The desired radius of the squircle. The default is 0.5. sides : int , optional The desired number of sides for the squircle. The default is 100. direction : list , optional The vector representing the up direction of the circle. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center". angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created squircle. """ def get_squircle(a=1, b=1, radius=0.5, sides=100): import numpy as np t = np.linspace(0, 2*np.pi, sides) x = (np.abs(np.cos(t))**(1/a)) * np.sign(np.cos(t)) y = (np.abs(np.sin(t))**(1/b)) * np.sign(np.sin(t)) return x*radius, y*radius from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Squircle - Error: The input origin parameter is not a valid Vertex. Retruning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Squircle - Error: The input placement parameter is not a recognised string. Retruning None.") return None radius = abs(radius) if radius < tolerance: return None if a <= 0: print("Wire.Squircle - Error: The a input parameter must be a positive number. Returning None.") return None if b <= 0: print("Wire.Squircle - Error: The b input parameter must be a positive number. Returning None.") return None if a == 1 and b == 1: return Wire.Circle(radius=radius, sides=sides, direction=direction, placement=placement, tolerance=tolerance) x_list, y_list = get_squircle(a=a, b=b, radius=radius, sides=sides) vertices = [] for i, x in enumerate(x_list): v = Vertex.ByCoordinates(x, y_list[i], 0) vertices.append(v) baseWire = Wire.ByVertices(vertices, close=True) baseWire = Topology.RemoveCollinearEdges(baseWire, angTolerance=angTolerance, tolerance=tolerance) baseWire = Wire.Simplify(baseWire, tolerance=tolerance) if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, radius, radius, 0) elif placement.lower() == "upperleft": baseWire = Topology.Translate(baseWire, radius, -radius, 0) elif placement.lower() == "lowerright": baseWire = Topology.Translate(baseWire, -radius, radius, 0) elif placement.lower() == "upperright": baseWire = Topology.Translate(baseWire, -radius, -radius, 0) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire @staticmethod def Star(origin= None, radiusA: float = 0.5, radiusB: float = 0.2, rays: int = 8, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a star. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the star. The default is None which results in the star being placed at (0, 0, 0). radiusA : float , optional The outer radius of the star. The default is 1.0. radiusB : float , optional The outer radius of the star. The default is 0.4. rays : int , optional The number of star rays. The default is 8. direction : list , optional The vector representing the up direction of the star. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the star. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created star. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None radiusA = abs(radiusA) radiusB = abs(radiusB) if radiusA < tolerance or radiusB < tolerance: return None rays = abs(rays) if rays < 3: return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: return None sides = rays*2 # Sides is double the number of rays baseV = [] xList = [] yList = [] for i in range(sides): if i%2 == 0: radius = radiusA else: radius = radiusB angle = math.radians(360/sides)*i x = math.sin(angle)*radius + origin.X() y = math.cos(angle)*radius + origin.Y() z = origin.Z() xList.append(x) yList.append(y) baseV.append([x, y]) if placement.lower() == "lowerleft": xmin = min(xList) ymin = min(yList) xOffset = origin.X() - xmin yOffset = origin.Y() - ymin elif placement.lower() == "upperleft": xmin = min(xList) ymax = max(yList) xOffset = origin.X() - xmin yOffset = origin.Y() - ymax elif placement.lower() == "lowerright": xmax = max(xList) ymin = min(yList) xOffset = origin.X() - xmax yOffset = origin.Y() - ymin elif placement.lower() == "upperright": xmax = max(xList) ymax = max(yList) xOffset = origin.X() - xmax yOffset = origin.Y() - ymax else: xOffset = 0 yOffset = 0 tranBase = [] for coord in baseV: tranBase.append(Vertex.ByCoordinates(coord[0]+xOffset, coord[1]+yOffset, origin.Z())) baseWire = Wire.ByVertices(tranBase[::-1], True) #reversing the list so that the normal points up in Blender if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire @staticmethod def StartEndVertices(wire) -> list: """ Returns the start and end vertices of the input wire. The wire must be manifold and open. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Topology import Topology if not Wire.IsManifold(wire): print("Wire.StartEndVertices - Error: The input wire parameter is non-manifold. Returning None.") return None vertices = Topology.Vertices(wire) if Wire.IsClosed(wire): return [vertices[0], vertices[0]] # If the wire is closed, the start and end vertices are the same vertex endPoints = [v for v in vertices if (Vertex.Degree(v, wire) == 1)] if len(endPoints) < 2: print("Wire.StartEndVertices - Error: Could not find the end vertices if the input wire parameter. Returning None.") return None edge1 = Topology.SuperTopologies(endPoints[0], wire, topologyType="edge")[0] sv = Edge.StartVertex(edge1) if (Topology.IsSame(endPoints[0], sv)): wireStartVertex = endPoints[0] wireEndVertex = endPoints[1] else: wireStartVertex = endPoints[1] wireEndVertex = endPoints[0] return [wireStartVertex, wireEndVertex] @staticmethod def StartVertex(wire): """ Returns the start vertex of the input wire. The wire must be manifold and open. """ sv, ev = Wire.StartEndVertices(wire) return sv @staticmethod def Trapezoid(origin= None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a trapezoid. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the trapezoid. The default is None which results in the trapezoid being placed at (0, 0, 0). widthA : float , optional The width of the bottom edge of the trapezoid. The default is 1.0. widthB : float , optional The width of the top edge of the trapezoid. The default is 0.75. offsetA : float , optional The offset of the bottom edge of the trapezoid. The default is 0.0. offsetB : float , optional The offset of the top edge of the trapezoid. The default is 0.0. length : float , optional The length of the trapezoid. The default is 1.0. direction : list , optional The vector representing the up direction of the trapezoid. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the trapezoid. This can be "center", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created trapezoid. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None widthA = abs(widthA) widthB = abs(widthB) length = abs(length) if widthA < tolerance or widthB < tolerance or length < tolerance: return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: return None xOffset = 0 yOffset = 0 if placement.lower() == "center": xOffset = -((-widthA*0.5 + offsetA) + (-widthB*0.5 + offsetB) + (widthA*0.5 + offsetA) + (widthB*0.5 + offsetB))/4.0 yOffset = 0 elif placement.lower() == "lowerleft": xOffset = -(min((-widthA*0.5 + offsetA), (-widthB*0.5 + offsetB))) yOffset = length*0.5 elif placement.lower() == "upperleft": xOffset = -(min((-widthA*0.5 + offsetA), (-widthB*0.5 + offsetB))) yOffset = -length*0.5 elif placement.lower() == "lowerright": xOffset = -(max((widthA*0.5 + offsetA), (widthB*0.5 + offsetB))) yOffset = length*0.5 elif placement.lower() == "upperright": xOffset = -(max((widthA*0.5 + offsetA), (widthB*0.5 + offsetB))) yOffset = -length*0.5 vb1 = Vertex.ByCoordinates(origin.X()-widthA*0.5+offsetA+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb2 = Vertex.ByCoordinates(origin.X()+widthA*0.5+offsetA+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb3 = Vertex.ByCoordinates(origin.X()+widthB*0.5+offsetB+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) vb4 = Vertex.ByCoordinates(origin.X()-widthB*0.5++offsetB+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], True) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire @staticmethod def VertexDistance(wire, vertex, origin= None, mantissa: int = 6, tolerance: float = 0.0001): """ Returns the distance, computed along the input wire of the input vertex from the input origin vertex. Parameters ---------- wire : topologic_core.Wire The input wire. vertex : topologic_core.Vertex The input vertex origin : topologic_core.Vertex , optional The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input wire. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- float The distance of the input vertex from the input origin along the input wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Topology import Topology if not Topology.IsInstance(wire, "Wire"): print("Wire.VertexDistance - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if not Topology.IsInstance(vertex, "Vertex"): print("Wire.VertexDistance - Error: The input vertex parameter is not a valid topologic vertex. Returning None.") return None wire_length = Wire.Length(wire) if wire_length < tolerance: print("Wire.VertexDistance: The input wire parameter is a degenerate topologic wire. Returning None.") return None if origin == None: origin = Wire.StartVertex(wire) if not Topology.IsInstance(origin, "Vertex"): print("Wire.VertexDistance - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None if not Vertex.IsInternal(vertex, wire, tolerance=tolerance): print("Wire.VertexDistance: The input vertex parameter is not internal to the input wire parameter. Returning None.") return None def distance_from_start(wire, v): total_distance = 0.0 found = False # Iterate over the edges of the wire for edge in Wire.Edges(wire): if Vertex.IsInternal(v, edge, tolerance=tolerance): total_distance += Vertex.Distance(Edge.StartVertex(edge), v) found = True break total_distance += Edge.Length(edge) if found == False: return None return total_distance d1 = distance_from_start(wire, vertex) d2 = distance_from_start(wire, origin) if d1 == None: print("Wire.VertexDistance - Error: The input vertex parameter is not internal to the input wire parameter. Returning None.") return None if d2 == None: print("Wire.VertexDistance - Error: The input origin parameter is not internal to the input wire parameter. Returning None.") return None return round(abs(d2-d1), mantissa) @staticmethod def VertexByDistance(wire, distance: float = 0.0, origin= None, tolerance = 0.0001): """ Creates a vertex along the input wire offset by the input distance from the input origin. Parameters ---------- edge : topologic_core.Edge The input edge. distance : float , optional The offset distance. The default is 0. origin : topologic_core.Vertex , optional The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input edge. The default is None. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Vertex The created vertex. """ from topologicpy.Vertex import Vertex def compute_u(u): def count_decimal_places(number): try: # Convert the number to a string to analyze decimal places num_str = str(number) # Split the number into integer and decimal parts integer_part, decimal_part = num_str.split('.') # Return the length of the decimal part return len(decimal_part) except ValueError: # If there's no decimal part, return 0 return 0 dp = count_decimal_places(u) u = -(int(u) - u) return round(u,dp) if not Topology.IsInstance(wire, "Wire"): print("Wire.VertexByDistance - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None wire_length = Wire.Length(wire) if wire_length < tolerance: print("Wire.VertexByDistance: The input wire parameter is a degenerate topologic wire. Returning None.") return None if abs(distance) < tolerance: return Wire.StartVertex(wire) if abs(distance - wire_length) < tolerance: return Wire.EndVertex(wire) if not Wire.IsManifold(wire): print("Wire.VertexAtParameter - Error: The input wire parameter is non-manifold. Returning None.") return None if origin == None: origin = Wire.StartVertex(wire) if not Topology.IsInstance(origin, "Vertex"): print("Wire.VertexByDistance - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None if not Vertex.IsInternal(origin, wire, tolerance=tolerance): print("Wire.VertexByDistance - Error: The input origin parameter is not internal to the input wire parameter. Returning None.") return None if Vertex.Distance(Wire.StartVertex(wire), origin) < tolerance: u = distance/wire_length elif Vertex.Distance(Wire.EndVertex(wire), origin) < tolerance: u = 1 - distance/wire_length else: d = Wire.VertexDistance(wire, origin) + distance u = d/wire_length return Wire.VertexByParameter(wire, u=compute_u(u)) @staticmethod def VertexByParameter(wire, u: float = 0): """ Creates a vertex along the input wire offset by the input *u* parameter. The wire must be manifold. Parameters ---------- wire : topologic_core.Wire The input wire. u : float , optional The *u* parameter along the input topologic Wire. A parameter of 0 returns the start vertex. A parameter of 1 returns the end vertex. The default is 0. Returns ------- topologic_core.Vertex The vertex at the input u parameter """ from topologicpy.Edge import Edge if not Topology.IsInstance(wire, "Wire"): print("Wire.VertexAtParameter - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if u < 0 or u > 1: print("Wire.VertexAtParameter - Error: The input u parameter is not within the valid range of [0, 1]. Returning None.") return None if not Wire.IsManifold(wire): print("Wire.VertexAtParameter - Error: The input wire parameter is non-manifold. Returning None.") return None if u == 0: return Wire.StartVertex(wire) if u == 1: return Wire.EndVertex(wire) edges = Wire.Edges(wire) total_length = 0.0 edge_lengths = [] # Compute the total length of the wire for edge in edges: e_length = Edge.Length(edge) edge_lengths.append(e_length) total_length += e_length # Initialize variables for tracking the current edge and accumulated length current_edge = None accumulated_length = 0.0 # Iterate over the lines to find the appropriate segment for i, edge in enumerate(edges): edge_length = edge_lengths[i] # Check if the desired point is on this line if u * total_length <= accumulated_length + edge_length: current_edge = edge break else: accumulated_length += edge_length # Calculate the residual u value for the current line residual_u = (u * total_length - accumulated_length) / Edge.Length(current_edge) # Compute the point at the parameter on the current line vertex = Edge.VertexByParameter(current_edge, residual_u) return vertex @staticmethod def Vertices(wire) -> list: """ Returns the list of vertices of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- list The list of vertices. """ if not Topology.IsInstance(wire, "Wire"): return None vertices = [] _ = wire.Vertices(None, vertices) return vertices
Ancestors
- topologicpy.Topology.Topology
Static methods
def Arc(startVertex, middleVertex, endVertex, sides: int = 16, close: bool = True, tolerance: float = 0.0001)
-
Creates an arc. The base chord will be parallel to the x-axis and the height will point in the positive y-axis direction.
Parameters
startVertex
:topologic_core.Vertex
- The location of the start vertex of the arc.
middleVertex
:topologic_core.Vertex
- The location of the middle vertex (apex) of the arc.
endVertex
:topologic_core.Vertex
- The location of the end vertex of the arc.
sides
:int
, optional- The number of sides of the arc. The default is 16.
close
:bool
, optional- If set to True, the arc will be closed by connecting the last vertex to the first vertex. Otherwise, it will be left open.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created arc.
Expand source code
@staticmethod def Arc(startVertex, middleVertex, endVertex, sides: int = 16, close: bool = True, tolerance: float = 0.0001): """ Creates an arc. The base chord will be parallel to the x-axis and the height will point in the positive y-axis direction. Parameters ---------- startVertex : topologic_core.Vertex The location of the start vertex of the arc. middleVertex : topologic_core.Vertex The location of the middle vertex (apex) of the arc. endVertex : topologic_core.Vertex The location of the end vertex of the arc. sides : int , optional The number of sides of the arc. The default is 16. close : bool , optional If set to True, the arc will be closed by connecting the last vertex to the first vertex. Otherwise, it will be left open. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created arc. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Topology import Topology def segmented_arc(x1, y1, x2, y2, x3, y3, sides): import math """ Generates a segmented arc passing through the three given points. Arguments: x1, y1: Coordinates of the first point x2, y2: Coordinates of the second point x3, y3: Coordinates of the third point sides: Number of sides to divide the arc Returns: List of tuples [x, y] representing the segmented arc passing through the points """ # Calculate the center of the circle A = x2 - x1 B = y2 - y1 C = x3 - x1 D = y3 - y1 E = A * (x1 + x2) + B * (y1 + y2) F = C * (x1 + x3) + D * (y1 + y3) G = 2 * (A * (y3 - y2) - B * (x3 - x2)) if G == 0: center_x = 0 center_y = 0 else: center_x = (D * E - B * F) / G center_y = (A * F - C * E) / G # Calculate the radius of the circle radius = math.sqrt((center_x - x1) ** 2 + (center_y - y1) ** 2) # Calculate the angles between the center and the three points angle1 = math.atan2(y1 - center_y, x1 - center_x) angle3 = math.atan2(y3 - center_y, x3 - center_x) # Calculate the angle between points 1 and 3 angle13 = (angle3 - angle1) % (2 * math.pi) if angle13 < 0: angle13 += 2 * math.pi # Determine the direction of the arc based on the angle between points 1 and 3 if angle13 < math.pi: start_angle = angle3 end_angle = angle1 else: start_angle = angle1 end_angle = angle3 # Calculate the angle increment angle_increment = (end_angle - start_angle) / sides # Generate the points of the arc passing through the points arc_points = [] for i in range(sides + 1): angle = start_angle + i * angle_increment x = center_x + radius * math.cos(angle) y = center_y + radius * math.sin(angle) arc_points.append([x, y]) return arc_points if not Topology.IsInstance(startVertex, "Vertex"): print("Wire.Arc - Error: The startVertex parameter is not a valid vertex. Returning None.") return None if not Topology.IsInstance(middleVertex, "Vertex"): print("Wire.Arc - Error: The middleVertex parameter is not a valid vertex. Returning None.") return None if not Topology.IsInstance(endVertex, "Vertex"): print("Wire.Arc - Error: The endVertex parameter is not a valid vertex. Returning None.") return None if Vertex.AreCollinear([startVertex, middleVertex, endVertex], tolerance = tolerance): return Wire.ByVertices([startVertex, middleVertex, endVertex], close=False) w = Wire.ByVertices([startVertex, middleVertex, endVertex], close=True) f = Face.ByWire(w, tolerance=tolerance) normal = Face.Normal(f) flat_w = Topology.Flatten(w, origin=startVertex, direction=normal) v1, v2, v3 = Topology.Vertices(flat_w) x1, y1, z1 = Vertex.Coordinates(v1) x2, y2, z2 = Vertex.Coordinates(v2) x3, y3, z3 = Vertex.Coordinates(v3) arc_points = segmented_arc(x1, y1, x2, y2, x3, y3, sides) arc_verts = [Vertex.ByCoordinates(coord[0], coord[1], 0) for coord in arc_points] arc = Wire.ByVertices(arc_verts, close=close) # Unflatten the arc arc = Topology.Unflatten(arc, origin=startVertex, direction=normal) return arc
def BoundingRectangle(topology, optimize: int = 0, tolerance=0.0001)
-
Returns a wire representing a bounding rectangle of the input topology. The returned wire contains a dictionary with key "zrot" that represents rotations around the Z axis. If applied the resulting wire will become axis-aligned.
Parameters
topology
:topologic_core.Topology
- The input topology.
optimize
:int
, optional- If set to an integer from 1 (low optimization) to 10 (high optimization), the method will attempt to optimize the bounding rectangle so that it reduces its surface area. The default is 0 which will result in an axis-aligned bounding rectangle. The default is 0.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The bounding rectangle of the input topology.
Expand source code
@staticmethod def BoundingRectangle(topology, optimize: int = 0, tolerance=0.0001): """ Returns a wire representing a bounding rectangle of the input topology. The returned wire contains a dictionary with key "zrot" that represents rotations around the Z axis. If applied the resulting wire will become axis-aligned. Parameters ---------- topology : topologic_core.Topology The input topology. optimize : int , optional If set to an integer from 1 (low optimization) to 10 (high optimization), the method will attempt to optimize the bounding rectangle so that it reduces its surface area. The default is 0 which will result in an axis-aligned bounding rectangle. The default is 0. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The bounding rectangle of the input topology. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import sample import time def br(topology): vertices = [] _ = topology.Vertices(None, vertices) x = [] y = [] for aVertex in vertices: x.append(aVertex.X()) y.append(aVertex.Y()) minX = min(x) minY = min(y) maxX = max(x) maxY = max(y) return [minX, minY, maxX, maxY] if not Topology.IsInstance(topology, "Topology"): return None world_origin = Vertex.Origin() vertices = Topology.SubTopologies(topology=topology, subTopologyType="vertex") start = time.time() period = 0 result = True while result and period < 30: vList = sample(vertices, 3) result = Vertex.AreCollinear(vList) end = time.time() period = end - start if result == True: print("Wire.BoundingRectangle - Error: Could not find three vertices that are not colinear within 30 seconds. Returning None.") return None w = Wire.ByVertices(vList) f = Face.ByWire(w, tolerance=tolerance) f_origin = Topology.Centroid(f) normal = Face.Normal(f) topology = Topology.Flatten(topology, origin=f_origin, direction=normal) boundingRectangle = br(topology) minX = boundingRectangle[0] minY = boundingRectangle[1] maxX = boundingRectangle[2] maxY = boundingRectangle[3] w = abs(maxX - minX) l = abs(maxY - minY) best_area = l*w orig_area = best_area best_z = 0 best_br = boundingRectangle origin = Topology.Centroid(topology) optimize = min(max(optimize, 0), 10) if optimize > 0: factor = (round(((11 - optimize)/30 + 0.57), 2)) flag = False for n in range(10, 0, -1): if flag: break za = n zb = 90+n zc = n for z in range(za,zb,zc): if flag: break t = Topology.Rotate(topology, origin=origin, axis=[0, 0, 1], angle=z) minX, minY, maxX, maxY = br(t) w = abs(maxX - minX) l = abs(maxY - minY) area = l*w if area < orig_area*factor: best_area = area best_z = z best_br = [minX, minY, maxX, maxY] flag = True break if area < best_area: best_area = area best_z = z best_br = [minX, minY, maxX, maxY] else: best_br = boundingRectangle minX, minY, maxX, maxY = best_br vb1 = Vertex.ByCoordinates(minX, minY, 0) vb2 = Vertex.ByCoordinates(maxX, minY, 0) vb3 = Vertex.ByCoordinates(maxX, maxY, 0) vb4 = Vertex.ByCoordinates(minX, maxY, 0) boundingRectangle = Wire.ByVertices([vb1, vb2, vb3, vb4], close=True) boundingRectangle = Topology.Rotate(boundingRectangle, origin=origin, axis=[0, 0, 1], angle=-best_z) boundingRectangle = Topology.Unflatten(boundingRectangle, origin=f_origin, direction=normal) dictionary = Dictionary.ByKeysValues(["zrot"], [best_z]) boundingRectangle = Topology.SetDictionary(boundingRectangle, dictionary) return boundingRectangle
def ByEdges(edges: list, orient: bool = False, tolerance: float = 0.0001)
-
Creates a wire from the input list of edges.
Parameters
edges
:list
- The input list of edges.
orient
:bool
, optional- If set to True the edges are oriented head to tail. Otherwise, they are not. The default is False.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001
Returns
topologic_core.Wire
- The created wire.
Expand source code
@staticmethod def ByEdges(edges: list, orient: bool = False, tolerance: float = 0.0001): """ Creates a wire from the input list of edges. Parameters ---------- edges : list The input list of edges. orient : bool , optional If set to True the edges are oriented head to tail. Otherwise, they are not. The default is False. tolerance : float , optional The desired tolerance. The default is 0.0001 Returns ------- topologic_core.Wire The created wire. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not isinstance(edges, list): return None edgeList = [x for x in edges if Topology.IsInstance(x, "Edge")] if len(edgeList) == 0: print("Wire.ByEdges - Error: The input edges list does not contain any valid edges. Returning None.") return None if len(edgeList) == 1: wire = topologic.Wire.ByEdges(edgeList) # Hook to Core else: wire = Topology.SelfMerge(Cluster.ByTopologies(edgeList), tolerance=tolerance) if not Topology.IsInstance(wire, "Wire"): print("Wire.ByEdges - Error: The operation failed. Returning None.") wire = None if Wire.IsManifold(wire): if orient == True: wire = Wire.OrientEdges(wire, Wire.StartVertex(wire), tolerance=tolerance) return wire
def ByEdgesCluster(cluster, tolerance: float = 0.0001)
-
Creates a wire from the input cluster of edges.
Parameters
cluster
:topologic_core.Cluster
- The input cluster of edges.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created wire.
Expand source code
@staticmethod def ByEdgesCluster(cluster, tolerance: float = 0.0001): """ Creates a wire from the input cluster of edges. Parameters ---------- cluster : topologic_core.Cluster The input cluster of edges. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire. """ if not Topology.IsInstance(cluster, "Cluster"): print("Wire.ByEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.") return None edges = [] _ = cluster.Edges(None, edges) return Wire.ByEdges(edges, tolerance=tolerance)
def ByOffset(wire, offset: float = 1.0, miter: bool = False, miterThreshold: float = None, offsetKey: str = None, miterThresholdKey: str = None, step: bool = True, angTolerance: float = 0.1, tolerance: float = 0.0001)
-
Creates an offset wire from the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
offset
:float
, optional- The desired offset distance. The default is 1.0.
miter
:bool
, optional- if set to True, the corners will be mitered. The default is False.
miterThreshold
:float
, optional- The distance beyond which a miter should be added. The default is None which means the miter threshold is set to the offset distance multiplied by the square root of 2.
offsetKey
:str
, optional- If specified, the dictionary of the edges will be queried for this key to specify the desired offset. The default is None.
miterThresholdKey
:str
, optional- If specified, the dictionary of the vertices will be queried for this key to specify the desired miter threshold distance. The default is None.
step
:bool
, optional- If set to True, The transition between collinear edges with different offsets will be a step. Otherwise, it will be a continous edge. The default is True.
angTolerance
:float
, optional- The desired angular tolerance. The default is 0.1.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created wire.
Expand source code
@staticmethod def ByOffset(wire, offset: float = 1.0, miter: bool = False, miterThreshold: float = None, offsetKey: str = None, miterThresholdKey: str = None, step: bool = True, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates an offset wire from the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. offset : float , optional The desired offset distance. The default is 1.0. miter : bool , optional if set to True, the corners will be mitered. The default is False. miterThreshold : float , optional The distance beyond which a miter should be added. The default is None which means the miter threshold is set to the offset distance multiplied by the square root of 2. offsetKey : str , optional If specified, the dictionary of the edges will be queried for this key to specify the desired offset. The default is None. miterThresholdKey : str , optional If specified, the dictionary of the vertices will be queried for this key to specify the desired miter threshold distance. The default is None. step : bool , optional If set to True, The transition between collinear edges with different offsets will be a step. Otherwise, it will be a continous edge. The default is True. angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import randrange, sample if not Topology.IsInstance(wire, "Wire"): return None if not miterThreshold: miterThreshold = offset*math.sqrt(2) flatFace = Face.ByWire(wire, tolerance=tolerance) origin = Topology.Centroid(flatFace) normal = Face.Normal(flatFace) flatFace = Topology.Flatten(flatFace, origin=origin, direction=normal) edges = Wire.Edges(wire) vertices = Wire.Vertices(wire) flatEdges = [] flatVertices = [] newEdges = [] for i in range(len(vertices)): flatVertex = Topology.Flatten(vertices[i], origin=origin, direction=normal) flatVertices.append(flatVertex) vertices = flatVertices for i in range(len(edges)): flatEdge = Topology.Flatten(edges[i], origin=origin, direction=normal) flatEdges.append(flatEdge) if offsetKey: d = Topology.Dictionary(edges[i]) value = Dictionary.ValueAtKey(d, key=offsetKey) c = Topology.Centroid(flatEdge) if value: finalOffset = value else: finalOffset = offset else: finalOffset = offset e1 = Edge.ByOffset2D(flatEdge,finalOffset) newEdges.append(e1) edges = flatEdges newVertices = [] dupVertices = [] if Wire.IsClosed(wire): e1 = newEdges[-1] e2 = newEdges[0] intV = Edge.Intersect2D(e1,e2) if intV: newVertices.append(intV) dupVertices.append(vertices[0]) elif step: edgeVertices= Edge.Vertices(e1) newVertices.append(Vertex.NearestVertex(vertices[-1], Cluster.ByTopologies(edgeVertices), useKDTree=False)) edgeVertices= Edge.Vertices(e2) newVertices.append(Vertex.NearestVertex(vertices[0], Cluster.ByTopologies(edgeVertices), useKDTree=False)) dupVertices.append(vertices[0]) dupVertices.append(vertices[0]) else: tempEdge1 = Edge.ByVertices([Edge.StartVertex(e1), Edge.EndVertex(e2)], tolerance=tolerance, silent=True) normal = Edge.Normal(e1) normal = [normal[0]*finalOffset*10, normal[1]*finalOffset*10, normal[2]*finalOffset*10] tempV = Vertex.ByCoordinates(vertices[0].X()+normal[0], vertices[0].Y()+normal[1], vertices[0].Z()+normal[2]) tempEdge2 = Edge.ByVertices([vertices[0], tempV], tolerance=tolerance, silent=True) intV = Edge.Intersect2D(tempEdge1,tempEdge2) newVertices.append(intV) dupVertices.append(vertices[0]) else: newVertices.append(Edge.StartVertex(newEdges[0])) for i in range(len(newEdges)-1): e1 = newEdges[i] e2 = newEdges[i+1] intV = Edge.Intersect2D(e1,e2) if intV: newVertices.append(intV) dupVertices.append(vertices[i+1]) elif step: newVertices.append(Edge.EndVertex(e1)) newVertices.append(Edge.StartVertex(e2)) dupVertices.append(vertices[i+1]) dupVertices.append(vertices[i+1]) else: tempEdge1 = Edge.ByVertices([Edge.StartVertex(e1), Edge.EndVertex(e2)], tolerance=tolerance, silent=True) normal = Edge.Normal(e1) normal = [normal[0]*finalOffset*10, normal[1]*finalOffset*10, normal[2]*finalOffset*10] tempV = Vertex.ByCoordinates(vertices[i+1].X()+normal[0], vertices[i+1].Y()+normal[1], vertices[i+1].Z()+normal[2]) tempEdge2 = Edge.ByVertices([vertices[i+1], tempV], tolerance=tolerance, silent=True) intV = Edge.Intersect2D(tempEdge1,tempEdge2) newVertices.append(intV) dupVertices.append(vertices[i+1]) vertices = dupVertices if not Wire.IsClosed(wire): newVertices.append(Edge.EndVertex(newEdges[-1])) newWire = Wire.ByVertices(newVertices, close=Wire.IsClosed(wire)) newVertices = Wire.Vertices(newWire) newEdges = Wire.Edges(newWire) miterEdges = [] cleanMiterEdges = [] # Handle miter if miter: for i in range(len(newVertices)): if miterThresholdKey: d = Topology.Dictionary(vertices[i]) value = Dictionary.ValueAtKey(d, key=miterThresholdKey) if value: finalMiterThreshold = value else: finalMiterThreshold = miterThreshold else: finalMiterThreshold = miterThreshold if Vertex.Distance(vertices[i], newVertices[i]) > abs(finalMiterThreshold): st = Topology.SuperTopologies(newVertices[i], newWire, topologyType="edge") if len(st) > 1: e1 = st[0] e2 = st[1] if not Edge.IsCollinear(e1, e2, tolerance=tolerance): e1 = Edge.Reverse(e1, tolerance=tolerance) bisector = Edge.ByVertices([vertices[i], newVertices[i]], tolerance=tolerance) nv = Edge.VertexByDistance(bisector, distance=finalMiterThreshold, origin=Edge.StartVertex(bisector), tolerance=0.0001) vec = Edge.Normal(bisector) nv2 = Topology.Translate(nv, vec[0], vec[1], 0) nv3 = Topology.Translate(nv, -vec[0], -vec[1], 0) miterEdge = Edge.ByVertices([nv2,nv3], tolerance=tolerance) if miterEdge: miterEdge = Edge.SetLength(miterEdge, abs(offset)*10) msv = Edge.Intersect2D(miterEdge, e1) mev = Edge.Intersect2D(miterEdge, e2) if (Vertex.IsInternal(msv, e1,tolerance=0.01) and (Vertex.IsInternal(mev, e2, tolerance=0.01))): miterEdge = Edge.ByVertices([msv, mev], tolerance=tolerance) if miterEdge: cleanMiterEdges.append(miterEdge) miterEdge = Edge.SetLength(miterEdge, Edge.Length(miterEdge)*1.02) miterEdges.append(miterEdge) c = Topology.SelfMerge(Cluster.ByTopologies(newEdges+miterEdges), tolerance=tolerance) vertices = Wire.Vertices(c) subtractEdges = [] for v in vertices: edges = Topology.SuperTopologies(v, c, topologyType="edge") if len(edges) == 2: if not Edge.IsCollinear(edges[0], edges[1], tolerance=tolerance): adjacentVertices = Topology.AdjacentTopologies(v, c) total = 0 for adjV in adjacentVertices: tempEdges = Topology.SuperTopologies(adjV, c, topologyType="edge") total += len(tempEdges) if total == 8: subtractEdges = subtractEdges+edges if len(subtractEdges) > 0: newWire = Topology.Boolean(newWire, Cluster.ByTopologies(subtractEdges), operation="difference", tolerance=tolerance) if len(cleanMiterEdges) > 0: newWire = Topology.Boolean(newWire, Cluster.ByTopologies(cleanMiterEdges), operation="merge", tolerance=tolerance) newWire = Topology.Unflatten(newWire, origin=origin, direction=normal) return newWire
def ByVertices(vertices: list, close: bool = True, tolerance: float = 0.0001)
-
Creates a wire from the input list of vertices.
Parameters
vertices
:list
- the input list of vertices.
close
:bool
, optional- If True the last vertex will be connected to the first vertex to close the wire. The default is True.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created wire.
Expand source code
@staticmethod def ByVertices(vertices: list, close: bool = True, tolerance: float = 0.0001): """ Creates a wire from the input list of vertices. Parameters ---------- vertices : list the input list of vertices. close : bool , optional If True the last vertex will be connected to the first vertex to close the wire. The default is True. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire. """ from topologicpy.Edge import Edge from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not isinstance(vertices, list): return None vertexList = [x for x in vertices if Topology.IsInstance(x, "Vertex")] if len(vertexList) < 2: print("Wire.ByVertices - Error: The number of vertices is less than 2. Returning None.") return None edges = [] for i in range(len(vertexList)-1): v1 = vertexList[i] v2 = vertexList[i+1] e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True) if Topology.IsInstance(e, "Edge"): edges.append(e) if close: v1 = vertexList[-1] v2 = vertexList[0] e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True) if Topology.IsInstance(e, "Edge"): edges.append(e) if len(edges) < 1: print("Wire.ByVertices - Error: The number of edges is less than 1. Returning None.") return None elif len(edges) == 1: wire = Wire.ByEdges(edges, orient=False) else: wire = Topology.SelfMerge(Cluster.ByTopologies(edges), tolerance=tolerance) return wire
def ByVerticesCluster(cluster, close: bool = True)
-
Creates a wire from the input cluster of vertices.
Parameters
cluster
:topologic_core.cluster
- the input cluster of vertices.
close
:bool
, optional- If True the last vertex will be connected to the first vertex to close the wire. The default is True.
Returns
topologic_core.Wire
- The created wire.
Expand source code
@staticmethod def ByVerticesCluster(cluster, close: bool = True): """ Creates a wire from the input cluster of vertices. Parameters ---------- cluster : topologic_core.cluster the input cluster of vertices. close : bool , optional If True the last vertex will be connected to the first vertex to close the wire. The default is True. Returns ------- topologic_core.Wire The created wire. """ if not Topology.IsInstance(cluster, "Cluster"): return None vertices = [] _ = cluster.Vertices(None, vertices) return Wire.ByVertices(vertices, close)
def Circle(origin=None, radius: float = 0.5, sides: int = 16, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a circle.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
radius
:float
, optional- The radius of the circle. The default is 0.5.
sides
:int
, optional- The number of sides of the circle. The default is 16.
fromAngle
:float
, optional- The angle in degrees from which to start creating the arc of the circle. The default is 0.
toAngle
:float
, optional- The angle in degrees at which to end creating the arc of the circle. The default is 360.
close
:bool
, optional- If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open.
direction
:list
, optional- The vector representing the up direction of the circle. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created circle.
Expand source code
@staticmethod def Circle(origin= None, radius: float = 0.5, sides: int = 16, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a circle. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0). radius : float , optional The radius of the circle. The default is 0.5. sides : int , optional The number of sides of the circle. The default is 16. fromAngle : float , optional The angle in degrees from which to start creating the arc of the circle. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the circle. The default is 360. close : bool , optional If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open. direction : list , optional The vector representing the up direction of the circle. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created circle. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Circle - Error: The input origin parameter is not a valid Vertex. Retruning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Circle - Error: The input placement parameter is not a recognised string. Retruning None.") return None radius = abs(radius) if radius < tolerance: return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: return None baseV = [] xList = [] yList = [] if toAngle < fromAngle: toAngle += 360 if abs(toAngle-fromAngle) < tolerance: return None angleRange = toAngle - fromAngle fromAngle = math.radians(fromAngle) toAngle = math.radians(toAngle) sides = int(math.floor(sides)) for i in range(sides+1): angle = fromAngle + math.radians(angleRange/sides)*i x = math.cos(angle)*radius + origin.X() y = math.sin(angle)*radius + origin.Y() z = origin.Z() xList.append(x) yList.append(y) baseV.append(Vertex.ByCoordinates(x, y, z)) if angleRange == 360: baseWire = Wire.ByVertices(baseV[::-1], close=False) #reversing the list so that the normal points up in Blender else: baseWire = Wire.ByVertices(baseV[::-1], close=close) #reversing the list so that the normal points up in Blender if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, radius, radius, 0) elif placement.lower() == "upperleft": baseWire = Topology.Translate(baseWire, radius, -radius, 0) elif placement.lower() == "lowerright": baseWire = Topology.Translate(baseWire, -radius, radius, 0) elif placement.lower() == "upperright": baseWire = Topology.Translate(baseWire, -radius, -radius, 0) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire
def Close(wire, mantissa=6, tolerance=0.0001)
-
Closes the input wire
Parameters
wire
:topologic_core.Wire
- The input wire.
mantissa
:int
, optional- The desired length of the mantissa. The default is 6.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The closed version of the input wire.
Expand source code
@staticmethod def Close(wire, mantissa=6, tolerance=0.0001): """ Closes the input wire Parameters ---------- wire : topologic_core.Wire The input wire. mantissa : int , optional The desired length of the mantissa. The default is 6. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The closed version of the input wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Helper import Helper def nearest_vertex(vertex, vertices): distances = [] for v in vertices: distances.append(Vertex.Distance(vertex, v)) new_vertices = Helper.Sort(vertices, distances) return new_vertices[1] #The first item is the same vertex, so return the next nearest vertex. if not Topology.IsInstance(wire, "Wire"): print("Wire.Close - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if Wire.IsClosed(wire): return wire vertices = Topology.Vertices(wire) ends = [v for v in vertices if Vertex.Degree(v, wire) == 1] if len(ends) < 2: print("Wire.Close - Error: The input wire parameter contains less than two open end vertices. Returning None.") return None geometry = Topology.Geometry(wire, mantissa=mantissa) g_vertices = geometry['vertices'] g_edges = geometry['edges'] used = [] for end in ends: nearest = nearest_vertex(end, ends) if not nearest in used: d = Vertex.Distance(end, nearest) i1 = Vertex.Index(end, vertices) i2 = Vertex.Index(nearest, vertices) if i1 == None or i2 == None: print("Wire.Close - Error: Something went wrong. Returning None.") return None if d < tolerance: g_vertices[i1] = Vertex.Coordinates(end) g_vertices[i2] = Vertex.Coordinates(end) else: if not(([i1, i2] in g_edges) or ([i2, i1] in g_edges)): g_edges.append([i1, i2]) used.append(end) new_wire = Topology.ByGeometry(vertices=g_vertices, edges=g_edges, faces=[], outputMode="wire") return new_wire
def ConvexHull(topology, tolerance: float = 0.0001)
-
Returns a wire representing the 2D convex hull of the input topology. The vertices of the topology are assumed to be coplanar.
Parameters
topology
:topologic_core.Topology
- The input topology.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The convex hull of the input topology.
Expand source code
@staticmethod def ConvexHull(topology, tolerance: float = 0.0001): """ Returns a wire representing the 2D convex hull of the input topology. The vertices of the topology are assumed to be coplanar. Parameters ---------- topology : topologic_core.Topology The input topology. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The convex hull of the input topology. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import sample def Left_index(points): ''' Finding the left most point ''' minn = 0 for i in range(1,len(points)): if points[i][0] < points[minn][0]: minn = i elif points[i][0] == points[minn][0]: if points[i][1] > points[minn][1]: minn = i return minn def orientation(p, q, r): ''' To find orientation of ordered triplet (p, q, r). The function returns following values 0 --> p, q and r are collinear 1 --> Clockwise 2 --> Counterclockwise ''' val = (q[1] - p[1]) * (r[0] - q[0]) - \ (q[0] - p[0]) * (r[1] - q[1]) if val == 0: return 0 elif val > 0: return 1 else: return 2 def convex_hull(points, n): # There must be at least 3 points if n < 3: return # Find the leftmost point l = Left_index(points) hull = [] ''' Start from leftmost point, keep moving counterclockwise until reach the start point again. This loop runs O(h) times where h is number of points in result or output. ''' p = l q = 0 while(True): # Add current point to result hull.append(p) ''' Search for a point 'q' such that orientation(p, q, x) is counterclockwise for all points 'x'. The idea is to keep track of last visited most counterclock- wise point in q. If any point 'i' is more counterclock- wise than q, then update q. ''' q = (p + 1) % n for i in range(n): # If i is more counterclockwise # than current q, then update q if(orientation(points[p], points[i], points[q]) == 2): q = i ''' Now q is the most counterclockwise with respect to p Set p as q for next iteration, so that q is added to result 'hull' ''' p = q # While we don't come to first point if(p == l): break # Print Result return hull f = None # Create a sample face and flatten while not Topology.IsInstance(f, "Face"): vertices = Topology.SubTopologies(topology=topology, subTopologyType="vertex") v = sample(vertices, 3) w = Wire.ByVertices(v) f = Face.ByWire(w, tolerance=tolerance) origin = Topology.Centroid(f) normal = Face.Normal(f) f = Topology.Flatten(f, origin=origin, direction=normal) topology = Topology.Flatten(topology, origin=origin, direction=normal) vertices = Topology.Vertices(topology) points = [] for v in vertices: points.append((Vertex.X(v), Vertex.Y(v))) hull = convex_hull(points, len(points)) hull_vertices = [] for p in hull: hull_vertices.append(Vertex.ByCoordinates(points[p][0], points[p][1], 0)) ch = Wire.ByVertices(hull_vertices) ch = Topology.Unflatten(ch, origin=origin, direction=normal) return ch
def Cycles(wire, maxVertices: int = 4, tolerance: float = 0.0001) ‑> list
-
Returns the closed circuits of wires found within the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
maxVertices
:int
, optional- The maximum number of vertices of the circuits to be searched. The default is 4.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
list
- The list of circuits (closed wires) found within the input wire.
Expand source code
@staticmethod def Cycles(wire, maxVertices: int = 4, tolerance: float = 0.0001) -> list: """ Returns the closed circuits of wires found within the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. maxVertices : int , optional The maximum number of vertices of the circuits to be searched. The default is 4. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- list The list of circuits (closed wires) found within the input wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge def vIndex(v, vList, tolerance): for i in range(len(vList)): if Vertex.Distance(v, vList[i]) < tolerance: return i+1 return None # rotate cycle path such that it begins with the smallest node def rotate_to_smallest(path): n = path.index(min(path)) return path[n:]+path[:n] def invert(path): return rotate_to_smallest(path[::-1]) def isNew(cycles, path): return not path in cycles def visited(node, path): return node in path def findNewCycles(graph, cycles, path, maxVertices): if len(path) > maxVertices: return start_node = path[0] next_node= None sub = [] #visit each edge and each node of each edge for edge in graph: node1, node2 = edge if start_node in edge: if node1 == start_node: next_node = node2 else: next_node = node1 if not visited(next_node, path): # neighbor node not on path yet sub = [next_node] sub.extend(path) # explore extended path findNewCycles(graph, cycles, sub, maxVertices); elif len(path) > 2 and next_node == path[-1]: # cycle found p = rotate_to_smallest(path); inv = invert(p) if isNew(cycles, p) and isNew(cycles, inv): cycles.append(p) def main(graph, cycles, maxVertices): returnValue = [] for edge in graph: for node in edge: findNewCycles(graph, cycles, [node], maxVertices) for cy in cycles: row = [] for node in cy: row.append(node) returnValue.append(row) return returnValue tEdges = [] _ = wire.Edges(None, tEdges) tVertices = [] _ = wire.Vertices(None, tVertices) tVertices = tVertices graph = [] for anEdge in tEdges: graph.append([vIndex(anEdge.StartVertex(), tVertices, tolerance), vIndex(anEdge.EndVertex(), tVertices, tolerance)]) cycles = [] resultingCycles = main(graph, cycles, maxVertices) result = [] for aRow in resultingCycles: row = [] for anIndex in aRow: row.append(tVertices[anIndex-1]) result.append(row) resultWires = [] for i in range(len(result)): c = result[i] resultEdges = [] for j in range(len(c)-1): v1 = c[j] v2 = c[j+1] e = Edge.ByStartVertexEndVertex(v1, v2, tolerance=tolerance, silent=True) resultEdges.append(e) e = Edge.ByStartVertexEndVertex(c[len(c)-1], c[0], tolerance=tolerance, silent=True) resultEdges.append(e) resultWire = Wire.ByEdges(resultEdges, tolerance=tolerance) resultWires.append(resultWire) return resultWires
def Edges(wire) ‑> list
-
Returns the edges of the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
Returns
list
- The list of edges.
Expand source code
@staticmethod def Edges(wire) -> list: """ Returns the edges of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- list The list of edges. """ if not Topology.IsInstance(wire, "Wire"): return None edges = [] _ = wire.Edges(None, edges) return edges
def Einstein(origin=None, radius: float = 0.5, direction: list = [0, 0, 1], placement: str = 'center')
-
Creates an aperiodic monotile, also called an 'einstein' tile (meaning one tile in German, not the name of the famous physist). See https://arxiv.org/abs/2303.10798
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the tile. The default is None which results in the tiles first vertex being placed at (0, 0, 0).
radius
:float
, optional- The radius of the hexagon determining the size of the tile. The default is 0.5.
direction
:list
, optional- The vector representing the up direction of the ellipse. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the hexagon determining the location of the tile. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
Expand source code
@staticmethod def Einstein(origin= None, radius: float = 0.5, direction: list = [0, 0, 1], placement: str = "center"): """ Creates an aperiodic monotile, also called an 'einstein' tile (meaning one tile in German, not the name of the famous physist). See https://arxiv.org/abs/2303.10798 Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the tile. The default is None which results in the tiles first vertex being placed at (0, 0, 0). radius : float , optional The radius of the hexagon determining the size of the tile. The default is 0.5. direction : list , optional The vector representing the up direction of the ellipse. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the hexagon determining the location of the tile. This can be "center", or "lowerleft". It is case insensitive. The default is "center". """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology import math def cos(angle): return math.cos(math.radians(angle)) def sin(angle): return math.sin(math.radians(angle)) if not origin: origin = Vertex.ByCoordinates(0, 0, 0) d = cos(30)*radius v1 = Vertex.ByCoordinates(0, 0, 0) v2 = Vertex.ByCoordinates(cos(30)*d, sin(30)*d, 0) v3 = Vertex.ByCoordinates(radius, 0) v4 = Vertex.ByCoordinates(2*radius, 0) v5 = Vertex.ByCoordinates(2*radius+cos(60)*radius*0.5, sin(30)*d, 0) v6 = Vertex.ByCoordinates(1.5*radius, d) v7 = Vertex.ByCoordinates(1.5*radius, 2*d) v8 = Vertex.ByCoordinates(radius, 2*d) v9 = Vertex.ByCoordinates(radius-cos(60)*0.5*radius, 2*d+sin(60)*0.5*radius) v10 = Vertex.ByCoordinates(0, 2*d) v11 = Vertex.ByCoordinates(0, d) v12 = Vertex.ByCoordinates(-radius*0.5, d) v13 = Vertex.ByCoordinates(-cos(30)*d, sin(30)*d, 0) einstein = Wire.ByVertices([v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13], close=True) if placement.lower() == "lowerleft": einstein = Topology.Translate(einstein, radius, d, 0) dx = Vertex.X(origin) dy = Vertex.Y(origin) dz = Vertex.Z(origin) einstein = Topology.Translate(einstein, dx, dy, dz) if direction != [0, 0, 1]: einstein = Topology.Orient(einstein, origin=origin, dirA=[0, 0, 1], dirB=direction) return einstein
def Ellipse(origin=None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: float = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates an ellipse and returns all its geometry and parameters.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0).
inputMode
:int
, optional- The method by wich the ellipse is defined. The default is 1. Based on the inputMode value, only the following inputs will be considered. The options are: 1. Width and Length (considered inputs: width, length) 2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity) 3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength) 4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength)
width
:float
, optional- The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1.
length
:float
, optional- The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1.
focalLength
:float
, optional- The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3.
eccentricity
:float
, optional- The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2.
majorAxisLength
:float
, optional- The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4.
minorAxisLength
:float
, optional- The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4.
sides
:int
, optional- The number of sides of the ellipse. The default is 32.
fromAngle
:float
, optional- The angle in degrees from which to start creating the arc of the ellipse. The default is 0.
toAngle
:float
, optional- The angle in degrees at which to end creating the arc of the ellipse. The default is 360.
close
:bool
, optional- If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open.
direction
:list
, optional- The vector representing the up direction of the ellipse. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created ellipse
Expand source code
@staticmethod def Ellipse(origin= None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: float = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates an ellipse and returns all its geometry and parameters. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0). inputMode : int , optional The method by wich the ellipse is defined. The default is 1. Based on the inputMode value, only the following inputs will be considered. The options are: 1. Width and Length (considered inputs: width, length) 2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity) 3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength) 4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength) width : float , optional The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1. length : float , optional The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1. focalLength : float , optional The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3. eccentricity : float , optional The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2. majorAxisLength : float , optional The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4. minorAxisLength : float , optional The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4. sides : int , optional The number of sides of the ellipse. The default is 32. fromAngle : float , optional The angle in degrees from which to start creating the arc of the ellipse. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the ellipse. The default is 360. close : bool , optional If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open. direction : list , optional The vector representing the up direction of the ellipse. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created ellipse """ ellipseAll = Wire.EllipseAll(origin=origin, inputMode=inputMode, width=width, length=length, focalLength=focalLength, eccentricity=eccentricity, majorAxisLength=majorAxisLength, minorAxisLength=minorAxisLength, sides=sides, fromAngle=fromAngle, toAngle=toAngle, close=close, direction=direction, placement=placement, tolerance=tolerance) return ellipseAll["ellipse"]
def EllipseAll(origin=None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates an ellipse and returns all its geometry and parameters.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0).
inputMode
:int
, optional- The method by wich the ellipse is defined. The default is 1. Based on the inputMode value, only the following inputs will be considered. The options are: 1. Width and Length (considered inputs: width, length) 2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity) 3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength) 4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength)
width
:float
, optional- The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1.
length
:float
, optional- The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1.
focalLength
:float
, optional- The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3.
eccentricity
:float
, optional- The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2.
majorAxisLength
:float
, optional- The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4.
minorAxisLength
:float
, optional- The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4.
sides
:int
, optional- The number of sides of the ellipse. The default is 32.
fromAngle
:float
, optional- The angle in degrees from which to start creating the arc of the ellipse. The default is 0.
toAngle
:float
, optional- The angle in degrees at which to end creating the arc of the ellipse. The default is 360.
close
:bool
, optional- If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open.
direction
:list
, optional- The vector representing the up direction of the ellipse. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
dictionary
- A dictionary with the following keys and values: 1. "ellipse" : The ellipse (topologic_core.Wire) 2. "foci" : The two focal points (topologic_core.Cluster containing two vertices) 3. "a" : The major axis length 4. "b" : The minor axis length 5. "c" : The focal length 6. "e" : The eccentricity 7. "width" : The width 8. "length" : The length
Expand source code
@staticmethod def EllipseAll(origin= None, inputMode: int = 1, width: float = 2.0, length: float = 1.0, focalLength: float = 0.866025, eccentricity: float = 0.866025, majorAxisLength: float = 1.0, minorAxisLength: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str ="center", tolerance: float = 0.0001): """ Creates an ellipse and returns all its geometry and parameters. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the ellipse. The default is None which results in the ellipse being placed at (0, 0, 0). inputMode : int , optional The method by wich the ellipse is defined. The default is 1. Based on the inputMode value, only the following inputs will be considered. The options are: 1. Width and Length (considered inputs: width, length) 2. Focal Length and Eccentricity (considered inputs: focalLength, eccentricity) 3. Focal Length and Minor Axis Length (considered inputs: focalLength, minorAxisLength) 4. Major Axis Length and Minor Axis Length (considered input: majorAxisLength, minorAxisLength) width : float , optional The width of the ellipse. The default is 2.0. This is considered if the inputMode is 1. length : float , optional The length of the ellipse. The default is 1.0. This is considered if the inputMode is 1. focalLength : float , optional The focal length of the ellipse. The default is 0.866025. This is considered if the inputMode is 2 or 3. eccentricity : float , optional The eccentricity of the ellipse. The default is 0.866025. This is considered if the inputMode is 2. majorAxisLength : float , optional The length of the major axis of the ellipse. The default is 1.0. This is considered if the inputMode is 4. minorAxisLength : float , optional The length of the minor axis of the ellipse. The default is 0.5. This is considered if the inputMode is 3 or 4. sides : int , optional The number of sides of the ellipse. The default is 32. fromAngle : float , optional The angle in degrees from which to start creating the arc of the ellipse. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the ellipse. The default is 360. close : bool , optional If set to True, arcs will be closed by connecting the last vertex to the first vertex. Otherwise, they will be left open. direction : list , optional The vector representing the up direction of the ellipse. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the ellipse. This can be "center", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- dictionary A dictionary with the following keys and values: 1. "ellipse" : The ellipse (topologic_core.Wire) 2. "foci" : The two focal points (topologic_core.Cluster containing two vertices) 3. "a" : The major axis length 4. "b" : The minor axis length 5. "c" : The focal length 6. "e" : The eccentricity 7. "width" : The width 8. "length" : The length """ from topologicpy.Vertex import Vertex from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None if inputMode not in [1, 2, 3, 4]: return None if placement.lower() not in ["center", "lowerleft"]: return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: return None width = abs(width) length = abs(length) focalLength= abs(focalLength) eccentricity=abs(eccentricity) majorAxisLength=abs(majorAxisLength) minorAxisLength=abs(minorAxisLength) sides = abs(sides) if width < tolerance or length < tolerance or focalLength < tolerance or eccentricity < tolerance or majorAxisLength < tolerance or minorAxisLength < tolerance or sides < 3: return None if inputMode == 1: w = width l = length a = width/2 b = length/2 c = math.sqrt(abs(b**2 - a**2)) e = c/a elif inputMode == 2: c = focalLength e = eccentricity a = c/e b = math.sqrt(abs(a**2 - c**2)) w = a*2 l = b*2 elif inputMode == 3: c = focalLength b = minorAxisLength a = math.sqrt(abs(b**2 + c**2)) e = c/a w = a*2 l = b*2 elif inputMode == 4: a = majorAxisLength b = minorAxisLength c = math.sqrt(abs(b**2 - a**2)) e = c/a w = a*2 l = b*2 else: return None baseV = [] xList = [] yList = [] if toAngle < fromAngle: toAngle += 360 if abs(toAngle - fromAngle) < tolerance: return None angleRange = toAngle - fromAngle fromAngle = math.radians(fromAngle) toAngle = math.radians(toAngle) sides = int(math.floor(sides)) for i in range(sides+1): angle = fromAngle + math.radians(angleRange/sides)*i x = math.sin(angle)*a + origin.X() y = math.cos(angle)*b + origin.Y() z = origin.Z() xList.append(x) yList.append(y) baseV.append(Vertex.ByCoordinates(x, y, z)) if angleRange == 360: baseWire = Wire.ByVertices(baseV[::-1], close=False) #reversing the list so that the normal points up in Blender else: baseWire = Wire.ByVertices(baseV[::-1], close=close) #reversing the list so that the normal points up in Blender if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, a, b, 0) baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) # Create a Cluster of the two foci v1 = Vertex.ByCoordinates(c+origin.X(), 0+origin.Y(), 0) v2 = Vertex.ByCoordinates(-c+origin.X(), 0+origin.Y(), 0) foci = Cluster.ByTopologies([v1, v2]) if placement.lower() == "lowerleft": foci = Topology.Translate(foci, a, b, 0) foci = Topology.Orient(foci, origin=origin, dirA=[0, 0, 1], dirB=direction) d = {} d['ellipse'] = baseWire d['foci'] = foci d['a'] = a d['b'] = b d['c'] = c d['e'] = e d['w'] = w d['l'] = l return d
def EndVertex(wire)
-
Returns the end vertex of the input wire. The wire must be manifold and open.
Expand source code
@staticmethod def EndVertex(wire): """ Returns the end vertex of the input wire. The wire must be manifold and open. """ sv, ev = Wire.StartEndVertices(wire) return ev
def ExteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) ‑> list
-
Returns the exterior angles of the input wire in degrees. The wire must be planar, manifold, and closed.
Parameters
wire
:topologic_core.Wire
- The input wire.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
mantissa
:int
, optional- The length of the desired mantissa. The default is 6.
Returns
list
- The list of exterior angles.
Expand source code
@staticmethod def ExteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) -> list: """ Returns the exterior angles of the input wire in degrees. The wire must be planar, manifold, and closed. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. mantissa : int , optional The length of the desired mantissa. The default is 6. Returns ------- list The list of exterior angles. """ if not Topology.IsInstance(wire, "Wire"): print("Wire.InteriorAngles - Error: The input wire parameter is not a valid wire. Returning None") return None if not Wire.IsManifold(wire): print("Wire.InteriorAngles - Error: The input wire parameter is non-manifold. Returning None") return None if not Wire.IsClosed(wire): print("Wire.InteriorAngles - Error: The input wire parameter is not closed. Returning None") return None interior_angles = Wire.InteriorAngles(wire, mantissa=mantissa) exterior_angles = [round(360-a, mantissa) for a in interior_angles] return exterior_angles
def Fillet(wire, radius: float = 0, radiusKey: str = None, tolerance: float = 0.0001, silent: bool = False)
-
Fillets (rounds) the interior and exterior corners of the input wire given the input radius. See https://en.wikipedia.org/wiki/Fillet_(mechanics)
Parameters
wire
:topologic_core.Wire
- The input wire.
radius
:float
- The desired radius of the fillet.
radiusKey
:str
, optional- If specified, the dictionary of the vertices will be queried for this key to specify the desired fillet radius. The default is None.
tolerance
:float
, optional- The desired tolerance. 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
topologic_core.Wire
- The filleted wire.
Expand source code
@staticmethod def Fillet(wire, radius: float = 0, radiusKey: str = None, tolerance: float = 0.0001, silent: bool = False): """ Fillets (rounds) the interior and exterior corners of the input wire given the input radius. See https://en.wikipedia.org/wiki/Fillet_(mechanics) Parameters ---------- wire : topologic_core.Wire The input wire. radius : float The desired radius of the fillet. radiusKey : str , optional If specified, the dictionary of the vertices will be queried for this key to specify the desired fillet radius. The default is None. tolerance : float , optional The desired tolerance. 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 ------- topologic_core.Wire The filleted wire. """ def start_from(edge, v): sv = Edge.StartVertex(edge) ev = Edge.EndVertex(edge) if Vertex.Distance(v, ev) < Vertex.Distance(v, sv): return Edge.Reverse(edge) return edge def compute_kite_edges(alpha, r): # Convert angle to radians alpha = math.radians(alpha) *0.5 h = r/math.cos(alpha) a = math.sqrt(h*h - r*r) return [a,h] import math from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Vector import Vector from topologicpy.Dictionary import Dictionary if not Topology.IsInstance(wire, "Wire"): if not silent: print("Wire.Fillet - Error: The input wire parameter is not a valid wire. Returning None.") return None if not Wire.IsManifold(wire): if not silent: print("Wire.Fillet - Error: The input wire parameter is not manifold. Returning None.") return None if not Topology.IsPlanar(wire): if not silent: print("Wire.Fillet - Error: The input wire parameter is not planar. Returning None.") return None orig_radius = radius f = Face.BoundingRectangle(wire, tolerance=tolerance) normal = Face.Normal(f) flat_wire = Topology.Flatten(wire, origin=Vertex.Origin(), direction=normal) vertices = Topology.Vertices(flat_wire) final_vertices = [] fillets = [] for v in vertices: radius = orig_radius edges = Topology.SuperTopologies(v, flat_wire, topologyType="edge") if len(edges) == 2: for edge in edges: ev = Edge.EndVertex(edge) if Vertex.Distance(v, ev) < tolerance: edge0 = edge else: edge1 = edge ang = Edge.Angle(edge0, edge1) e1 = start_from(edge0, v) e2 = start_from(edge1, v) dir1 = Edge.Direction(e1) dir2 = Edge.Direction(e2) if Vector.IsParallel(dir1, dir2) or Vector.IsAntiParallel(dir1, dir2): pass else: if isinstance(radiusKey, str): d = Topology.Dictionary(v) if Topology.IsInstance(d, "Dictionary"): v_radius = Dictionary.ValueAtKey(d, radiusKey) if isinstance(v_radius, float) or isinstance(v_radius, int): if v_radius >= 0: radius = v_radius if radius > 0: dir_bisector = Vector.Bisect(dir1,dir2) a, h = compute_kite_edges(ang, radius) if a <= Edge.Length(e1) and a <= Edge.Length(e2): v1 = Topology.TranslateByDirectionDistance(v, dir1, a) center = Topology.TranslateByDirectionDistance(v, dir_bisector, h) v2 = Topology.TranslateByDirectionDistance(v, dir2, a) r1 = Edge.ByVertices(center, v1) dir1 = Edge.Direction(r1) r2 = Edge.ByVertices(center, v2) dir2 = Edge.Direction(r2) compass1 = Vector.CompassAngle(Vector.East(), dir1)*-1 compass2 = Vector.CompassAngle(Vector.East(), dir2)*-1 if compass2 < compass1: temp = compass2 compass2 = compass1 compass1 = temp w1 = Wire.Circle(origin=center, radius=radius, fromAngle=compass1, toAngle=compass2, close=False) w2 = Wire.Circle(origin=center, radius=radius, fromAngle=compass2, toAngle=compass1, close=False) if Wire.Length(w1) < Wire.Length(w2): fillet = w1 else: fillet = w2 f_sv = Wire.StartVertex(fillet) if Vertex.Distance(f_sv, edge1) < Vertex.Distance(f_sv, edge0): fillet = Wire.Reverse(fillet) final_vertices += Topology.Vertices(fillet) else: if not silent: print("Wire.Fillet - Error: The specified fillet radius is too large to be applied. Skipping.") else: final_vertices.append(v) else: final_vertices.append(v) flat_wire = Wire.ByVertices(final_vertices, close=Wire.IsClosed(wire)) # Unflatten the wire return_wire = Topology.Unflatten(flat_wire, origin=Vertex.Origin(), direction=normal) return return_wire
def InteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) ‑> list
-
Returns the interior angles of the input wire in degrees. The wire must be planar, manifold, and closed.
Parameters
wire
:topologic_core.Wire
- The input wire.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
mantissa
:int
, optional- The desired length of the mantissa. The default is 6.
Returns
list
- The list of interior angles.
Expand source code
@staticmethod def InteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) -> list: """ Returns the interior angles of the input wire in degrees. The wire must be planar, manifold, and closed. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- list The list of interior angles. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Topology import Topology from topologicpy.Vector import Vector from topologicpy.Dictionary import Dictionary if not Topology.IsInstance(wire, "Wire"): print("Wire.InteriorAngles - Error: The input wire parameter is not a valid wire. Returning None") return None if not Wire.IsManifold(wire): print("Wire.InteriorAngles - Error: The input wire parameter is non-manifold. Returning None") return None if not Wire.IsClosed(wire): print("Wire.InteriorAngles - Error: The input wire parameter is not closed. Returning None") return None f = Face.ByWire(wire) normal = Face.Normal(f) origin = Topology.Centroid(f) w = Topology.Flatten(wire, origin=origin, direction=normal) angles = [] edges = Topology.Edges(w) for i in range(len(edges)-1): e1 = edges[i] e2 = edges[i+1] a = round(360 - Vector.CompassAngle(Edge.Direction(e1), Edge.Direction(e2)), mantissa) angles.append(a) e1 = edges[len(edges)-1] e2 = edges[0] a = round(360 - Vector.CompassAngle(Edge.Direction(e1), Edge.Direction(e2)), mantissa) angles = [a]+angles return angles
def Interpolate(wires: list, n: int = 5, outputType: str = 'default', mapping: str = 'default', tolerance: float = 0.0001)
-
Creates n number of wires that interpolate between wireA and wireB.
Parameters
wireA
:topologic_core.Wire
- The first input wire.
wireB
:topologic_core.Wire
- The second input wire.
n
:int
, optional- The number of intermediate wires to create. The default is 5.
outputType
:str
, optional- The desired type of output. The options are case insensitive. The default is "contour". The options are: - "Default" or "Contours" (wires are not connected) - "Raster or "Zigzag" or "Toolpath" (the wire ends are connected to create a continous path) - "Grid" (the wire ends are connected to create a grid).
mapping
:str
, optional- The desired type of mapping for wires with different number of vertices. It is case insensitive. The default is "default". The options are: - "Default" or "Repeat" which repeats the last vertex of the wire with the least number of vertices - "Nearest" which maps the vertices of one wire to the nearest vertex of the next wire creating a list of equal number of vertices.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
tTopology
- The created interpolated wires as well as the input wires. The return type can be a topologic_core.Cluster or a topologic_core.Wire based on options.
Expand source code
@staticmethod def Interpolate(wires: list, n: int = 5, outputType: str = "default", mapping: str = "default", tolerance: float = 0.0001): """ Creates *n* number of wires that interpolate between wireA and wireB. Parameters ---------- wireA : topologic_core.Wire The first input wire. wireB : topologic_core.Wire The second input wire. n : int , optional The number of intermediate wires to create. The default is 5. outputType : str , optional The desired type of output. The options are case insensitive. The default is "contour". The options are: - "Default" or "Contours" (wires are not connected) - "Raster or "Zigzag" or "Toolpath" (the wire ends are connected to create a continous path) - "Grid" (the wire ends are connected to create a grid). mapping : str , optional The desired type of mapping for wires with different number of vertices. It is case insensitive. The default is "default". The options are: - "Default" or "Repeat" which repeats the last vertex of the wire with the least number of vertices - "Nearest" which maps the vertices of one wire to the nearest vertex of the next wire creating a list of equal number of vertices. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- tTopology The created interpolated wires as well as the input wires. The return type can be a topologic_core.Cluster or a topologic_core.Wire based on options. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Helper import Helper outputType = outputType.lower() if outputType not in ["default", "contours", "raster", "zigzag", "toolpath", "grid"]: return None if outputType == "default" or outputType == "contours": outputType = "contours" if outputType == "raster" or outputType == "zigzag" or outputType == "toolpath": outputType = "zigzag" mapping = mapping.lower() if mapping not in ["default", "nearest", "repeat"]: print("Wire.Interpolate - Error: The mapping input parameter is not recognized. Returning None.") return None def nearestVertex(v, vertices): distances = [Vertex.Distance(v, vertex) for vertex in vertices] return vertices[distances.index(sorted(distances)[0])] def replicate(vertices, mapping="default"): vertices = Helper.Repeat(vertices) finalList = vertices if mapping == "nearest": finalList = [vertices[0]] for i in range(len(vertices)-1): loopA = vertices[i] loopB = vertices[i+1] nearestVertices = [] for j in range(len(loopA)): nv = nearestVertex(loopA[j], loopB) nearestVertices.append(nv) finalList.append(nearestVertices) return finalList def process(verticesA, verticesB, n=5): contours = [verticesA] for i in range(1, n+1): u = float(i)/float(n+1) temp_vertices = [] for j in range(len(verticesA)): temp_v = Edge.VertexByParameter(Edge.ByVertices([verticesA[j], verticesB[j]], tolerance=tolerance), u) temp_vertices.append(temp_v) contours.append(temp_vertices) return contours if len(wires) < 2: return None vertices = [] for wire in wires: vertices.append(Topology.SubTopologies(wire, subTopologyType="vertex")) vertices = replicate(vertices, mapping=mapping) contours = [] finalWires = [] for i in range(len(vertices)-1): verticesA = vertices[i] verticesB = vertices[i+1] contour = process(verticesA=verticesA, verticesB=verticesB, n=n) contours += contour for c in contour: finalWires.append(Wire.ByVertices(c, Wire.IsClosed(wires[i]))) contours.append(vertices[-1]) finalWires.append(wires[-1]) ridges = [] if outputType == "grid" or outputType == "zigzag": for i in range(len(contours)-1): verticesA = contours[i] verticesB = contours[i+1] if outputType == "grid": for j in range(len(verticesA)): ridges.append(Edge.ByVertices([verticesA[j], verticesB[j]], tolerance=tolerance)) elif outputType == "zigzag": if i%2 == 0: sv = verticesA[-1] ev = verticesB[-1] ridges.append(Edge.ByVertices([sv, ev], tolerance=tolerance)) else: sv = verticesA[0] ev = verticesB[0] ridges.append(Edge.ByVertices([sv, ev], tolerance=tolerance)) return Topology.SelfMerge(Cluster.ByTopologies(finalWires+ridges), tolerance=tolerance)
def Invert(wire)
-
Creates a wire that is an inverse (mirror) of the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
Returns
topologic_core.Wire
- The inverted wire.
Expand source code
@staticmethod def Invert(wire): """ Creates a wire that is an inverse (mirror) of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- topologic_core.Wire The inverted wire. """ if not Topology.IsInstance(wire, "Wire"): return None vertices = Wire.Vertices(wire) reversed_vertices = vertices[::-1] return Wire.ByVertices(reversed_vertices)
def IsClosed(wire) ‑> bool
-
Returns True if the input wire is closed. Returns False otherwise.
Parameters
wire
:topologic_core.Wire
- The input wire.
Returns
bool
- True if the input wire is closed. False otherwise.
Expand source code
@staticmethod def IsClosed(wire) -> bool: """ Returns True if the input wire is closed. Returns False otherwise. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- bool True if the input wire is closed. False otherwise. """ status = None if wire: if Topology.IsInstance(wire, "Wire"): status = wire.IsClosed() return status
def IsManifold(wire) ‑> bool
-
Returns True if the input wire is manifold. Returns False otherwise. A manifold wire is one where its vertices have a degree of 1 or 2.
Parameters
wire
:topologic_core.Wire
- The input wire.
Returns
bool
- True if the input wire is manifold. False otherwise.
Expand source code
@staticmethod def IsManifold(wire) -> bool: """ Returns True if the input wire is manifold. Returns False otherwise. A manifold wire is one where its vertices have a degree of 1 or 2. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- bool True if the input wire is manifold. False otherwise. """ from topologicpy.Vertex import Vertex if not Topology.IsInstance(wire, "Wire"): print("Wire.IsManifold - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None vertices = Wire.Vertices(wire) for v in vertices: if Vertex.Degree(v, hostTopology=wire) > 2: return False return True
def IsSimilar(wireA, wireB, angTolerance: float = 0.1, tolerance: float = 0.0001) ‑> bool
-
Returns True if the input wires are similar. Returns False otherwise. The wires must be closed.
Parameters
wireA
:topologic_core.Wire
- The first input wire.
wireB
:topologic_core.Wire
- The second input wire.
angTolerance
:float
, optional- The desired angular tolerance. The default is 0.1.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
bool
- True if the two input wires are similar. False otherwise.
Expand source code
@staticmethod def IsSimilar(wireA, wireB, angTolerance: float = 0.1, tolerance: float = 0.0001) -> bool: """ Returns True if the input wires are similar. Returns False otherwise. The wires must be closed. Parameters ---------- wireA : topologic_core.Wire The first input wire. wireB : topologic_core.Wire The second input wire. angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- bool True if the two input wires are similar. False otherwise. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge def isCyclicallyEquivalent(u, v, lengthTolerance, angleTolerance): n, i, j = len(u), 0, 0 if n != len(v): return False while i < n and j < n: if (i % 2) == 0: tol = lengthTolerance else: tol = angleTolerance k = 1 while k <= n and math.fabs(u[(i + k) % n]- v[(j + k) % n]) <= tol: k += 1 if k > n: return True if math.fabs(u[(i + k) % n]- v[(j + k) % n]) > tol: i += k else: j += k return False def angleBetweenEdges(e1, e2, tolerance): a = e1.EndVertex().X() - e1.StartVertex().X() b = e1.EndVertex().Y() - e1.StartVertex().Y() c = e1.EndVertex().Z() - e1.StartVertex().Z() d = Vertex.Distance(e1.EndVertex(), e2.StartVertex()) if d <= tolerance: d = e2.StartVertex().X() - e2.EndVertex().X() e = e2.StartVertex().Y() - e2.EndVertex().Y() f = e2.StartVertex().Z() - e2.EndVertex().Z() else: d = e2.EndVertex().X() - e2.StartVertex().X() e = e2.EndVertex().Y() - e2.StartVertex().Y() f = e2.EndVertex().Z() - e2.StartVertex().Z() dotProduct = a*d + b*e + c*f modOfVector1 = math.sqrt( a*a + b*b + c*c)*math.sqrt(d*d + e*e + f*f) angle = dotProduct/modOfVector1 angleInDegrees = math.degrees(math.acos(angle)) return angleInDegrees def getInteriorAngles(edges, tolerance): angles = [] for i in range(len(edges)-1): e1 = edges[i] e2 = edges[i+1] angles.append(angleBetweenEdges(e1, e2, tolerance)) return angles def getRep(edges, tolerance): angles = getInteriorAngles(edges, tolerance) lengths = [] for anEdge in edges: lengths.append(Edge.Length(anEdge)) minLength = min(lengths) normalisedLengths = [] for aLength in lengths: normalisedLengths.append(aLength/minLength) return [x for x in itertools.chain(*itertools.zip_longest(normalisedLengths, angles)) if x is not None] if (wireA.IsClosed() == False): return None if (wireB.IsClosed() == False): return None edgesA = [] _ = wireA.Edges(None, edgesA) edgesB = [] _ = wireB.Edges(None, edgesB) if len(edgesA) != len(edgesB): return False repA = getRep(list(edgesA), tolerance) repB = getRep(list(edgesB), tolerance) if isCyclicallyEquivalent(repA, repB, tolerance, angTolerance): return True if isCyclicallyEquivalent(repA, repB[::-1], tolerance, angTolerance): return True return False
def Length(wire, mantissa: int = 6) ‑> float
-
Returns the length of the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
mantissa
:int
, optional- The desired length of the mantissa. The default is 6.
Returns
float
- The length of the input wire. Test
Expand source code
@staticmethod def Length(wire, mantissa: int = 6) -> float: """ Returns the length of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- float The length of the input wire. Test """ from topologicpy.Edge import Edge if not wire: return None if not Topology.IsInstance(wire, "Wire"): return None totalLength = None try: edges = [] _ = wire.Edges(None, edges) totalLength = 0 for anEdge in edges: totalLength = totalLength + Edge.Length(anEdge) totalLength = round(totalLength, mantissa) except: totalLength = None return totalLength
def Line(origin=None, length: float = 1, direction: list = [1, 0, 0], sides: int = 2, placement: str = 'center')
-
Creates a straight line wire using the input parameters.
Parameters
origin
:topologic_core.Vertex
, optional- The origin location of the box. The default is None which results in the edge being placed at (0, 0, 0).
length
:float
, optional- The desired length of the edge. The default is 1.0.
direction
:list
, optional- The desired direction (vector) of the edge. The default is [1, 0, 0] (along the X-axis).
sides
:int
, optional- The desired number of sides/segments. The minimum number of sides is 2. The default is 2.
placement
:str
, optional- The desired placement of the edge. The options are: 1. "center" which places the center of the edge at the origin. 2. "start" which places the start of the edge at the origin. 3. "end" which places the end of the edge at the origin. The default is "center".
Returns
topologic_core.Edge
- The created edge
Expand source code
@staticmethod def Line(origin= None, length: float = 1, direction: list = [1, 0, 0], sides: int = 2, placement: str ="center"): """ Creates a straight line wire using the input parameters. Parameters ---------- origin : topologic_core.Vertex , optional The origin location of the box. The default is None which results in the edge being placed at (0, 0, 0). length : float , optional The desired length of the edge. The default is 1.0. direction : list , optional The desired direction (vector) of the edge. The default is [1, 0, 0] (along the X-axis). sides : int , optional The desired number of sides/segments. The minimum number of sides is 2. The default is 2. placement : str , optional The desired placement of the edge. The options are: 1. "center" which places the center of the edge at the origin. 2. "start" which places the start of the edge at the origin. 3. "end" which places the end of the edge at the origin. The default is "center". Returns ------- topologic_core.Edge The created edge """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Vector import Vector from topologicpy.Topology import Topology if origin == None: origin = Vertex.Origin() if not Topology.IsInstance(origin, "Vertex"): print("Wire.Line - Error: The input origin is not a valid vertex. Returning None.") return None if length <= 0: print("Wire.Line - Error: The input length is less than or equal to zero. Returning None.") return None if not isinstance(direction, list): print("Wire.Line - Error: The input direction is not a valid list. Returning None.") return None if not len(direction) == 3: print("Wire.Line - Error: The length of the input direction is not equal to three. Returning None.") return None if sides < 2: print("Wire.Line - Error: The number of sides cannot be less than two. Consider using Edge.Line() instead. Returning None.") return None edge = Edge.Line(origin=origin, length=length, direction=direction, placement=placement) vertices = [Edge.StartVertex(edge)] unitDistance = float(1)/float(sides) for i in range(1, sides): vertices.append(Edge.VertexByParameter(edge, i*unitDistance)) vertices.append(Edge.EndVertex(edge)) return Wire.ByVertices(vertices)
def OrientEdges(wire, vertexA, tolerance=0.0001)
-
Returns a correctly oriented head-to-tail version of the input wire. The input wire must be manifold.
Parameters
wire
:topologic_core.Wire
- The input wire.
vertexA
:topologic_core.Vertex
- The desired start vertex of the wire.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The oriented wire.
Expand source code
@staticmethod def OrientEdges(wire, vertexA, tolerance=0.0001): """ Returns a correctly oriented head-to-tail version of the input wire. The input wire must be manifold. Parameters ---------- wire : topologic_core.Wire The input wire. vertexA : topologic_core.Vertex The desired start vertex of the wire. tolerance : float, optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The oriented wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge if not Topology.IsInstance(wire, "Wire"): print("Wire.OrientEdges - Error: The input wire parameter is not a valid wire. Returning None.") return None if not Topology.IsInstance(vertexA, "Vertex"): print("Wire.OrientEdges - Error: The input vertexA parameter is not a valid vertex. Returning None.") return None if not Wire.IsManifold(wire): print("Wire.OrientEdges - Error: The input wire parameter is not a manifold wire. Returning None.") return None oriented_edges = [] remaining_edges = Topology.Edges(wire) current_vertex = vertexA while remaining_edges: next_edge = None for edge in remaining_edges: if Vertex.Distance(Edge.StartVertex(edge), current_vertex) < tolerance: next_edge = edge break elif Vertex.Distance(Edge.EndVertex(edge), current_vertex) < tolerance: next_edge = Edge.Reverse(edge) break if next_edge: oriented_edges.append(next_edge) remaining_edges.remove(next_edge) current_vertex = Edge.EndVertex(next_edge) else: # Unable to find a next edge connected to the current vertex break vertices = [Edge.StartVertex(oriented_edges[0])] for i, edge in enumerate(oriented_edges): vertices.append(Edge.EndVertex(edge)) return Wire.ByVertices(vertices, close=Wire.IsClosed(wire))
def Planarize(wire, origin=None, mantissa: int = 6, tolerance: float = 0.0001)
-
Returns a planarized version of the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
origin
:topologic_core.Vertex
, optional- The desired origin of the plane unto which the planar wire will be projected. If set to None, the centroid of the input wire will be chosen. The default is None.
mantissa
:int
, optional- The desired length of the mantissa. The default is 6.
Returns
topologic_core.Wire
- The planarized wire.
Expand source code
@staticmethod def Planarize(wire, origin= None, mantissa: int = 6, tolerance: float = 0.0001): """ Returns a planarized version of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float, optional The desired tolerance. The default is 0.0001. origin : topologic_core.Vertex , optional The desired origin of the plane unto which the planar wire will be projected. If set to None, the centroid of the input wire will be chosen. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- topologic_core.Wire The planarized wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(wire, "Wire"): print("Wire.Planarize - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if origin == None: origin = Vertex.Origin() if not Topology.IsInstance(origin, "Vertex"): print("Wire.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None vertices = Topology.Vertices(wire) edges = Topology.Edges(wire) plane_equation = Vertex.PlaneEquation(vertices, mantissa=mantissa) rect = Face.RectangleByPlaneEquation(origin=origin , equation=plane_equation, tolerance=tolerance) new_vertices = [Vertex.Project(v, rect, mantissa=mantissa) for v in vertices] new_vertices = Vertex.Fuse(new_vertices, mantissa=mantissa, tolerance=tolerance) new_edges = [] for edge in edges: sv = Edge.StartVertex(edge) ev = Edge.EndVertex(edge) sv1 = Vertex.Project(sv, rect) i = Vertex.Index(sv1, new_vertices, tolerance=tolerance) if i: sv1 = new_vertices[i] ev1 = Vertex.Project(ev, rect) i = Vertex.Index(ev1, new_vertices, tolerance=tolerance) if i: ev1 = new_vertices[i] new_edges.append(Edge.ByVertices([sv1, ev1])) return Topology.SelfMerge(Cluster.ByTopologies(new_edges), tolerance=tolerance)
def Project(wire, face, direction: list = None, mantissa: int = 6, tolerance: float = 0.0001)
-
Creates a projection of the input wire unto the input face.
Parameters
wire
:topologic_core.Wire
- The input wire.
face
:topologic_core.Face
- The face unto which to project the input wire.
direction
:list
, optional- The vector representing the direction of the projection. If None, the reverse vector of the receiving face normal will be used. The default is None.
mantissa
:int
, optional- The desired length of the mantissa. The default is 6.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The projected wire.
Expand source code
@staticmethod def Project(wire, face, direction: list = None, mantissa: int = 6, tolerance: float = 0.0001): """ Creates a projection of the input wire unto the input face. Parameters ---------- wire : topologic_core.Wire The input wire. face : topologic_core.Face The face unto which to project the input wire. direction : list, optional The vector representing the direction of the projection. If None, the reverse vector of the receiving face normal will be used. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The projected wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Topology import Topology if not wire: return None if not Topology.IsInstance(wire, "Wire"): return None if not face: return None if not Topology.IsInstance(face, "Face"): return None if not direction: direction = -1*Face.NormalAtParameters(face, 0.5, 0.5, "XYZ", mantissa) large_face = Topology.Scale(face, face.CenterOfMass(), 500, 500, 500) edges = [] _ = wire.Edges(None, edges) projected_edges = [] if large_face: if (Topology.Type(large_face) == Topology.TypeID("Face")): for edge in edges: if edge: if (Topology.Type(edge) == Topology.TypeID("Edge")): sv = edge.StartVertex() ev = edge.EndVertex() psv = Vertex.Project(vertex=sv, face=large_face, direction=direction) pev = Vertex.Project(vertex=ev, face=large_face, direction=direction) if psv and pev: try: pe = Edge.ByVertices([psv, pev], tolerance=tolerance) projected_edges.append(pe) except: continue w = Wire.ByEdges(projected_edges, tolerance=tolerance) return w
def Rectangle(origin=None, width: float = 1.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = 'center', angTolerance: float = 0.1, tolerance: float = 0.0001)
-
Creates a rectangle.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0).
width
:float
, optional- The width of the rectangle. The default is 1.0.
length
:float
, optional- The length of the rectangle. The default is 1.0.
direction
:list
, optional- The vector representing the up direction of the rectangle. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the rectangle. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
angTolerance
:float
, optional- The desired angular tolerance. The default is 0.1.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created rectangle.
Expand source code
@staticmethod def Rectangle(origin= None, width: float = 1.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates a rectangle. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0). width : float , optional The width of the rectangle. The default is 1.0. length : float , optional The length of the rectangle. The default is 1.0. direction : list , optional The vector representing the up direction of the rectangle. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the rectangle. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center". angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created rectangle. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Rectangle - Error: specified origin is not a topologic vertex. Retruning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Rectangle - Error: Could not find placement in the list of placements. Retruning None.") return None width = abs(width) length = abs(length) if width < tolerance or length < tolerance: print("Wire.Rectangle - Error: One or more of the specified dimensions is below the tolerance value. Retruning None.") return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: print("Wire.Rectangle - Error: The direction vector magnitude is below the tolerance value. Retruning None.") return None xOffset = 0 yOffset = 0 if placement.lower() == "lowerleft": xOffset = width*0.5 yOffset = length*0.5 elif placement.lower() == "upperleft": xOffset = width*0.5 yOffset = -length*0.5 elif placement.lower() == "lowerright": xOffset = -width*0.5 yOffset = length*0.5 elif placement.lower() == "upperright": xOffset = -width*0.5 yOffset = -length*0.5 vb1 = Vertex.ByCoordinates(origin.X()-width*0.5+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb2 = Vertex.ByCoordinates(origin.X()+width*0.5+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb3 = Vertex.ByCoordinates(origin.X()+width*0.5+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) vb4 = Vertex.ByCoordinates(origin.X()-width*0.5+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], True) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire
def RemoveCollinearEdges(wire, angTolerance: float = 0.1, tolerance: float = 0.0001)
-
Removes any collinear edges in the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
angTolerance
:float
, optional- The desired angular tolerance. The default is 0.1.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created wire without any collinear edges.
Expand source code
@staticmethod def RemoveCollinearEdges(wire, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Removes any collinear edges in the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created wire without any collinear edges. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Wire import Wire from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology def cleanup(wire, tolerance): vertices = Topology.Vertices(wire) vertices = Vertex.Fuse(vertices, tolerance=tolerance) edges = Topology.Edges(wire) new_edges = [] for edge in edges: sv = Edge.StartVertex(edge) sv = vertices[Vertex.Index(sv, vertices)] ev = Edge.EndVertex(edge) ev = vertices[Vertex.Index(ev, vertices)] new_edges.append(Edge.ByVertices([sv,ev])) new_wire = Topology.SelfMerge(Cluster.ByTopologies(new_edges), tolerance=tolerance) return new_wire def rce(wire, angTolerance=0.1): if not Topology.IsInstance(wire, "Wire"): return wire final_wire = None vertices = [] wire_verts = [] try: _ = wire.Vertices(None, vertices) except: return wire for aVertex in vertices: edges = [] _ = aVertex.Edges(wire, edges) if len(edges) > 1: if not Edge.IsCollinear(edges[0], edges[1], tolerance=tolerance): wire_verts.append(aVertex) else: wire_verts.append(aVertex) if len(wire_verts) > 2: if wire.IsClosed(): final_wire = Wire.ByVertices(wire_verts, close=True) else: final_wire = Wire.ByVertices(wire_verts, close=False) elif len(wire_verts) == 2: final_wire = Edge.ByStartVertexEndVertex(wire_verts[0], wire_verts[1], tolerance=tolerance, silent=True) return final_wire new_wire = cleanup(wire, tolerance=tolerance) if not Wire.IsManifold(new_wire): wires = Wire.Split(new_wire) else: wires = [new_wire] returnWires = [] for aWire in wires: if not Topology.IsInstance(aWire, "Wire"): returnWires.append(aWire) else: returnWires.append(rce(aWire, angTolerance=angTolerance)) if len(returnWires) == 1: returnWire = returnWires[0] if Topology.IsInstance(returnWire, "Edge"): return Wire.ByEdges([returnWire], tolerance=tolerance) elif Topology.IsInstance(returnWire, "Wire"): return returnWire else: return wire elif len(returnWires) > 1: returnWire = Topology.SelfMerge(Cluster.ByTopologies(returnWires)) if Topology.IsInstance(returnWire, "Edge"): return Wire.ByEdges([returnWire], tolerance=tolerance) elif Topology.IsInstance(returnWire, "Wire"): return returnWire else: return wire else: return wire
def Reverse(wire, tolerance: float = 0.0001)
-
Creates a wire that has the reverse direction of the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The reversed wire.
Expand source code
@staticmethod def Reverse(wire, tolerance: float = 0.0001): """ Creates a wire that has the reverse direction of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The reversed wire. """ from topologicpy.Topology import Topology if not Topology.IsInstance(wire, "Wire"): print("Wire.Reverse - Error: The input wire parameter is not a valid wire. Returning None.") return None if not Wire.IsManifold(wire): print("Wire.Reverse - Error: The input wire parameter is not a manifold wire. Returning None.") return None vertices = Topology.Vertices(wire) vertices.reverse() new_wire = Wire.ByVertices(vertices, close=Wire.IsClosed(wire), tolerance=tolerance) return new_wire
def Roof(face, angle: float = 45, tolerance: float = 0.001)
-
Creates a hipped roof through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao gaoxipeng1998@gmail.com This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
Parameters
face
:topologic_core.Face
- The input face.
angle
:float , optioal
- The desired angle in degrees of the roof. The default is 45.
tolerance
:float
, optional- The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
Returns
topologic_core.Wire
- The created roof. This method returns the roof as a set of edges. No faces are created.
Expand source code
@staticmethod def Roof(face, angle: float = 45, tolerance: float = 0.001): """ Creates a hipped roof through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com> This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel Parameters ---------- face : topologic_core.Face The input face. angle : float , optioal The desired angle in degrees of the roof. The default is 45. tolerance : float , optional The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better) Returns ------- topologic_core.Wire The created roof. This method returns the roof as a set of edges. No faces are created. """ from topologicpy import Polyskel from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from topologicpy.Helper import Helper import topologic_core as topologic import math def subtrees_to_edges(subtrees, polygon, slope): polygon_z = {} for x, y, z in polygon: polygon_z[(x, y)] = z edges = [] for subtree in subtrees: source = subtree.source height = subtree.height z = slope * height source_vertex = Vertex.ByCoordinates(source.x, source.y, z) for sink in subtree.sinks: if (sink.x, sink.y) in polygon_z: z = 0 else: z = None for st in subtrees: if st.source.x == sink.x and st.source.y == sink.y: z = slope * st.height break for sk in st.sinks: if sk.x == sink.x and sk.y == sink.y: z = slope * st.height break if z is None: height = subtree.height z = slope * height sink_vertex = Vertex.ByCoordinates(sink.x, sink.y, z) if (source.x, source.y) == (sink.x, sink.y): continue e = Edge.ByStartVertexEndVertex(source_vertex, sink_vertex, tolerance=tolerance, silent=True) if e not in edges and e != None: edges.append(e) return edges def face_to_skeleton(face, angle=0): normal = Face.Normal(face) eb_wire = Face.ExternalBoundary(face) ib_wires = Face.InternalBoundaries(face) eb_vertices = Topology.Vertices(eb_wire) if normal[2] > 0: eb_vertices = list(reversed(eb_vertices)) eb_polygon_coordinates = [(v.X(), v.Y(), v.Z()) for v in eb_vertices] eb_polygonxy = [(x[0], x[1]) for x in eb_polygon_coordinates] ib_polygonsxy = [] zero_coordinates = eb_polygon_coordinates for ib_wire in ib_wires: ib_vertices = Topology.Vertices(ib_wire) if normal[2] > 0: ib_vertices = list(reversed(ib_vertices)) ib_polygon_coordinates = [(v.X(), v.Y(), v.Z()) for v in ib_vertices] ib_polygonxy = [(x[0], x[1]) for x in ib_polygon_coordinates] ib_polygonsxy.append(ib_polygonxy) zero_coordinates += ib_polygon_coordinates skeleton = Polyskel.skeletonize(eb_polygonxy, ib_polygonsxy) slope = math.tan(math.radians(angle)) roofEdges = subtrees_to_edges(skeleton, zero_coordinates, slope) roofEdges = Helper.Flatten(roofEdges)+Topology.Edges(face) roofTopology = Topology.SelfMerge(Cluster.ByTopologies(roofEdges), tolerance=tolerance) return roofTopology if not Topology.IsInstance(face, "Face"): return None angle = abs(angle) if angle >= 90-tolerance: return None origin = Topology.Centroid(face) normal = Face.Normal(face) flat_face = Topology.Flatten(face, origin=origin, direction=normal) d = Topology.Dictionary(flat_face) roof = face_to_skeleton(flat_face, angle) if not roof: return None roof = Topology.Unflatten(roof, origin=origin, direction=normal) return roof
def Simplify(wire, tolerance=0.0001)
-
Simplifies the input wire edges based on the Douglas Peucker algorthim. See https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm Part of this code was contributed by gaoxipeng. See https://github.com/wassimj/topologicpy/issues/35
Parameters
wire
:topologic_core.Wire
- The input wire.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed.
Returns
topologic_core.Wire
- The simplified wire.
Expand source code
@staticmethod def Simplify(wire, tolerance=0.0001): """ Simplifies the input wire edges based on the Douglas Peucker algorthim. See https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm Part of this code was contributed by gaoxipeng. See https://github.com/wassimj/topologicpy/issues/35 Parameters ---------- wire : topologic_core.Wire The input wire. tolerance : float , optional The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed. Returns ------- topologic_core.Wire The simplified wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology def perpendicular_distance(point, line_start, line_end): # Calculate the perpendicular distance from a point to a line segment x0 = point.X() y0 = point.Y() x1 = line_start.X() y1 = line_start.Y() x2 = line_end.X() y2 = line_end.Y() numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) denominator = Vertex.Distance(line_start, line_end) return numerator / denominator def douglas_peucker(wire, tolerance): if isinstance(wire, list): points = wire else: points = Wire.Vertices(wire) # points.insert(0, points.pop()) if len(points) <= 2: return points # Use the first and last points in the list as the starting and ending points start_point = points[0] end_point = points[-1] # Find the point with the maximum distance max_distance = 0 max_index = 0 for i in range(1, len(points) - 1): d = perpendicular_distance(points[i], start_point, end_point) if d > max_distance: max_distance = d max_index = i # If the maximum distance is less than the tolerance, no further simplification is needed if max_distance <= tolerance: return [start_point, end_point] # Recursively simplify first_segment = douglas_peucker(points[:max_index + 1], tolerance) second_segment = douglas_peucker(points[max_index:], tolerance) # Merge the two simplified segments return first_segment[:-1] + second_segment if not Topology.IsInstance(wire, "Wire"): print("Wire.Simplify = Error: The input wire parameter is not a Wire. Returning None.") return None if not Wire.IsManifold(wire): wires = Wire.Split(wire) new_wires = [] for w in wires: if Topology.IsInstance(w, "Edge"): if Edge.Length(w) > tolerance: new_wires.append(w) elif Topology.IsInstance(w, "Wire"): new_wires.append(Wire.Simplify(w, tolerance=tolerance)) return_wire = Topology.SelfMerge(Cluster.ByTopologies(new_wires)) return return_wire new_vertices = douglas_peucker(wire, tolerance=tolerance) new_wire = Wire.ByVertices(new_vertices, close=Wire.IsClosed(wire)) return new_wire
def Skeleton(face, tolerance=0.001)
-
Creates a straight skeleton. This method is contributed by 高熙鹏 xipeng gao gaoxipeng1998@gmail.com This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
Parameters
face
:topologic_core.Face
- The input face.
tolerance
:float
, optional- The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
Returns
topologic_core.Wire
- The created straight skeleton.
Expand source code
@staticmethod def Skeleton(face, tolerance=0.001): """ Creates a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com> This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel Parameters ---------- face : topologic_core.Face The input face. tolerance : float , optional The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better) Returns ------- topologic_core.Wire The created straight skeleton. """ if not Topology.IsInstance(face, "Face"): return None return Wire.Roof(face, angle=0, tolerance=tolerance)
def Spiral(origin=None, radiusA: float = 0.05, radiusB: float = 0.5, height: float = 1, turns: int = 10, sides: int = 36, clockwise: bool = False, reverse: bool = False, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a spiral.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the spiral. The default is None which results in the spiral being placed at (0, 0, 0).
radiusA
:float
, optional- The initial radius of the spiral. The default is 0.05.
radiusB
:float
, optional- The final radius of the spiral. The default is 0.5.
height
:float
, optional- The height of the spiral. The default is 1.
turns
:int
, optional- The number of turns of the spiral. The default is 10.
sides
:int
, optional- The number of sides of one full turn in the spiral. The default is 36.
clockwise
:bool
, optional- If set to True, the spiral will be oriented in a clockwise fashion. Otherwise, it will be oriented in an anti-clockwise fashion. The default is False.
reverse
:bool
, optional- If set to True, the spiral will increase in height from the center to the circumference. Otherwise, it will increase in height from the conference to the center. The default is False.
direction
:list
, optional- The vector representing the up direction of the spiral. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the spiral. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
Returns
topologic_core.Wire
- The created spiral.
Expand source code
@staticmethod def Spiral(origin = None, radiusA : float = 0.05, radiusB : float = 0.5, height : float = 1, turns : int = 10, sides : int = 36, clockwise : bool = False, reverse : bool = False, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a spiral. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the spiral. The default is None which results in the spiral being placed at (0, 0, 0). radiusA : float , optional The initial radius of the spiral. The default is 0.05. radiusB : float , optional The final radius of the spiral. The default is 0.5. height : float , optional The height of the spiral. The default is 1. turns : int , optional The number of turns of the spiral. The default is 10. sides : int , optional The number of sides of one full turn in the spiral. The default is 36. clockwise : bool , optional If set to True, the spiral will be oriented in a clockwise fashion. Otherwise, it will be oriented in an anti-clockwise fashion. The default is False. reverse : bool , optional If set to True, the spiral will increase in height from the center to the circumference. Otherwise, it will increase in height from the conference to the center. The default is False. direction : list , optional The vector representing the up direction of the spiral. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the spiral. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center". Returns ------- topologic_core.Wire The created spiral. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology import math if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Spiral - Error: the input origin is not a valid topologic Vertex. Returning None.") return None if radiusA <= 0: print("Wire.Spiral - Error: the input radiusA cannot be less than or equal to zero. Returning None.") return None if radiusB <= 0: print("Wire.Spiral - Error: the input radiusB cannot be less than or equal to zero. Returning None.") return None if radiusA == radiusB: print("Wire.Spiral - Error: the inputs radiusA and radiusB cannot be equal. Returning None.") return None if radiusB > radiusA: temp = radiusA radiusA = radiusB radiusB = temp if turns <= 0: print("Wire.Spiral - Error: the input turns cannot be less than or equal to zero. Returning None.") return None if sides < 3: print("Wire.Spiral - Error: the input sides cannot be less than three. Returning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Spiral - Error: the input placement string is not one of center, lowerleft, upperleft, lowerright, or upperright. Returning None.") return None if (abs(direction[0]) + abs(direction[1]) + abs(direction[2])) < tolerance: print("Wire.Spiral - Error: the input direction vector is not a valid direction. Returning None.") return None vertices = [] xList = [] yList = [] zList = [] if clockwise: cw = -1 else: cw = 1 n_vertices = sides*turns + 1 zOffset = height/float(n_vertices) if reverse == True: z = height else: z = 0 ang = 0 angOffset = float(360/float(sides)) b = (radiusB - radiusA)/(2*math.pi*turns) while ang <= 360*turns: rad = math.radians(ang) x = (radiusA + b*rad)*math.cos(rad)*cw xList.append(x) y = (radiusA + b*rad)*math.sin(rad) yList.append(y) zList.append(z) if reverse == True: z = z - zOffset else: z = z + zOffset vertices.append(Vertex.ByCoordinates(x, y, z)) ang = ang + angOffset minX = min(xList) maxX = max(xList) minY = min(yList) maxY = max(yList) radius = radiusA + radiusB*turns*0.5 baseWire = Wire.ByVertices(vertices, close=False) if placement.lower() == "center": baseWire = Topology.Translate(baseWire, 0, 0, -height*0.5) if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, -minX, -minY, 0) elif placement.lower() == "upperleft": baseWire = Topology.Translate(baseWire, -minX, -maxY, 0) elif placement.lower() == "lowerright": baseWire = Topology.Translate(baseWire, -maxX, -minY, 0) elif placement.lower() == "upperright": baseWire = Topology.Translate(baseWire, -maxX, -maxY, 0) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire
def Split(wire) ‑> list
-
Splits the input wire into segments at its intersections (i.e. at any vertex where more than two edges meet).
Parameters
wire
:topologic_core.Wire
- The input wire.
Returns
list
- The list of split wire segments.
Expand source code
@staticmethod def Split(wire) -> list: """ Splits the input wire into segments at its intersections (i.e. at any vertex where more than two edges meet). Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- list The list of split wire segments. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology def vertexDegree(v, wire): edges = [] _ = v.Edges(wire, edges) return len(edges) def vertexOtherEdge(vertex, edge, wire): edges = [] _ = vertex.Edges(wire, edges) if Topology.IsSame(edges[0], edge): return edges[-1] else: return edges[0] def edgeOtherVertex(edge, vertex): vertices = [] _ = edge.Vertices(None, vertices) if Topology.IsSame(vertex, vertices[0]): return vertices[-1] else: return vertices[0] def edgeInList(edge, edgeList): for anEdge in edgeList: if Topology.IsSame(anEdge, edge): return True return False vertices = [] _ = wire.Vertices(None, vertices) hubs = [] for aVertex in vertices: if vertexDegree(aVertex, wire) > 2: hubs.append(aVertex) wires = [] global_edges = [] for aVertex in hubs: hub_edges = [] _ = aVertex.Edges(wire, hub_edges) wire_edges = [] for hub_edge in hub_edges: if not edgeInList(hub_edge, global_edges): current_edge = hub_edge oe = edgeOtherVertex(current_edge, aVertex) while vertexDegree(oe, wire) == 2: if not edgeInList(current_edge, global_edges): global_edges.append(current_edge) wire_edges.append(current_edge) current_edge = vertexOtherEdge(oe, current_edge, wire) oe = edgeOtherVertex(current_edge, oe) if not edgeInList(current_edge, global_edges): global_edges.append(current_edge) wire_edges.append(current_edge) if len(wire_edges) > 1: wires.append(Cluster.ByTopologies(wire_edges).SelfMerge()) else: wires.append(wire_edges[0]) wire_edges = [] if len(wires) < 1: return [wire] return wires
def Square(origin=None, size: float = 1.0, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a square.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0).
size
:float
, optional- The size of the square. The default is 1.0.
direction
:list
, optional- The vector representing the up direction of the square. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the square. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created square.
Expand source code
@staticmethod def Square(origin= None, size: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a square. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0). size : float , optional The size of the square. The default is 1.0. direction : list , optional The vector representing the up direction of the square. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the square. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created square. """ return Wire.Rectangle(origin=origin, width=size, length=size, direction=direction, placement=placement, tolerance=tolerance)
def Squircle(origin=None, radius: float = 0.5, sides: int = 121, a: float = 2.0, b: float = 2.0, direction: list = [0, 0, 1], placement: str = 'center', angTolerance: float = 0.1, tolerance: float = 0.0001)
-
Creates a Squircle which is a hybrid between a circle and a square. See https://en.wikipedia.org/wiki/Squircle
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the squircle. The default is None which results in the squircle being placed at (0, 0, 0).
radius
:float
, optional- The radius of the squircle. The default is 0.5.
sides
:int
, optional- The number of sides of the squircle. The default is 121.
a
:float
, optional- The "a" factor affects the x position of the points to interpolate between a circle and a square. A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
b
:float
, optional- The "b" factor affects the y position of the points to interpolate between a circle and a square. A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0.
radius
:float
, optional- The desired radius of the squircle. The default is 0.5.
sides
:int
, optional- The desired number of sides for the squircle. The default is 100.
direction
:list
, optional- The vector representing the up direction of the circle. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
angTolerance
:float
, optional- The desired angular tolerance. The default is 0.1.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created squircle.
Expand source code
@staticmethod def Squircle(origin = None, radius: float = 0.5, sides: int = 121, a: float = 2.0, b: float = 2.0, direction: list = [0, 0, 1], placement: str = "center", angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates a Squircle which is a hybrid between a circle and a square. See https://en.wikipedia.org/wiki/Squircle Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the squircle. The default is None which results in the squircle being placed at (0, 0, 0). radius : float , optional The radius of the squircle. The default is 0.5. sides : int , optional The number of sides of the squircle. The default is 121. a : float , optional The "a" factor affects the x position of the points to interpolate between a circle and a square. A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0. b : float , optional The "b" factor affects the y position of the points to interpolate between a circle and a square. A value of 1 will create a circle. Higher values will create a more square-like shape. The default is 2.0. radius : float , optional The desired radius of the squircle. The default is 0.5. sides : int , optional The desired number of sides for the squircle. The default is 100. direction : list , optional The vector representing the up direction of the circle. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center". angTolerance : float , optional The desired angular tolerance. The default is 0.1. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created squircle. """ def get_squircle(a=1, b=1, radius=0.5, sides=100): import numpy as np t = np.linspace(0, 2*np.pi, sides) x = (np.abs(np.cos(t))**(1/a)) * np.sign(np.cos(t)) y = (np.abs(np.sin(t))**(1/b)) * np.sign(np.sin(t)) return x*radius, y*radius from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): print("Wire.Squircle - Error: The input origin parameter is not a valid Vertex. Retruning None.") return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: print("Wire.Squircle - Error: The input placement parameter is not a recognised string. Retruning None.") return None radius = abs(radius) if radius < tolerance: return None if a <= 0: print("Wire.Squircle - Error: The a input parameter must be a positive number. Returning None.") return None if b <= 0: print("Wire.Squircle - Error: The b input parameter must be a positive number. Returning None.") return None if a == 1 and b == 1: return Wire.Circle(radius=radius, sides=sides, direction=direction, placement=placement, tolerance=tolerance) x_list, y_list = get_squircle(a=a, b=b, radius=radius, sides=sides) vertices = [] for i, x in enumerate(x_list): v = Vertex.ByCoordinates(x, y_list[i], 0) vertices.append(v) baseWire = Wire.ByVertices(vertices, close=True) baseWire = Topology.RemoveCollinearEdges(baseWire, angTolerance=angTolerance, tolerance=tolerance) baseWire = Wire.Simplify(baseWire, tolerance=tolerance) if placement.lower() == "lowerleft": baseWire = Topology.Translate(baseWire, radius, radius, 0) elif placement.lower() == "upperleft": baseWire = Topology.Translate(baseWire, radius, -radius, 0) elif placement.lower() == "lowerright": baseWire = Topology.Translate(baseWire, -radius, radius, 0) elif placement.lower() == "upperright": baseWire = Topology.Translate(baseWire, -radius, -radius, 0) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire
def Star(origin=None, radiusA: float = 0.5, radiusB: float = 0.2, rays: int = 8, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a star.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the star. The default is None which results in the star being placed at (0, 0, 0).
radiusA
:float
, optional- The outer radius of the star. The default is 1.0.
radiusB
:float
, optional- The outer radius of the star. The default is 0.4.
rays
:int
, optional- The number of star rays. The default is 8.
direction
:list
, optional- The vector representing the up direction of the star. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the star. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created star.
Expand source code
@staticmethod def Star(origin= None, radiusA: float = 0.5, radiusB: float = 0.2, rays: int = 8, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a star. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the star. The default is None which results in the star being placed at (0, 0, 0). radiusA : float , optional The outer radius of the star. The default is 1.0. radiusB : float , optional The outer radius of the star. The default is 0.4. rays : int , optional The number of star rays. The default is 8. direction : list , optional The vector representing the up direction of the star. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the star. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created star. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None radiusA = abs(radiusA) radiusB = abs(radiusB) if radiusA < tolerance or radiusB < tolerance: return None rays = abs(rays) if rays < 3: return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: return None sides = rays*2 # Sides is double the number of rays baseV = [] xList = [] yList = [] for i in range(sides): if i%2 == 0: radius = radiusA else: radius = radiusB angle = math.radians(360/sides)*i x = math.sin(angle)*radius + origin.X() y = math.cos(angle)*radius + origin.Y() z = origin.Z() xList.append(x) yList.append(y) baseV.append([x, y]) if placement.lower() == "lowerleft": xmin = min(xList) ymin = min(yList) xOffset = origin.X() - xmin yOffset = origin.Y() - ymin elif placement.lower() == "upperleft": xmin = min(xList) ymax = max(yList) xOffset = origin.X() - xmin yOffset = origin.Y() - ymax elif placement.lower() == "lowerright": xmax = max(xList) ymin = min(yList) xOffset = origin.X() - xmax yOffset = origin.Y() - ymin elif placement.lower() == "upperright": xmax = max(xList) ymax = max(yList) xOffset = origin.X() - xmax yOffset = origin.Y() - ymax else: xOffset = 0 yOffset = 0 tranBase = [] for coord in baseV: tranBase.append(Vertex.ByCoordinates(coord[0]+xOffset, coord[1]+yOffset, origin.Z())) baseWire = Wire.ByVertices(tranBase[::-1], True) #reversing the list so that the normal points up in Blender if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire
def StartEndVertices(wire) ‑> list
-
Returns the start and end vertices of the input wire. The wire must be manifold and open.
Expand source code
@staticmethod def StartEndVertices(wire) -> list: """ Returns the start and end vertices of the input wire. The wire must be manifold and open. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Topology import Topology if not Wire.IsManifold(wire): print("Wire.StartEndVertices - Error: The input wire parameter is non-manifold. Returning None.") return None vertices = Topology.Vertices(wire) if Wire.IsClosed(wire): return [vertices[0], vertices[0]] # If the wire is closed, the start and end vertices are the same vertex endPoints = [v for v in vertices if (Vertex.Degree(v, wire) == 1)] if len(endPoints) < 2: print("Wire.StartEndVertices - Error: Could not find the end vertices if the input wire parameter. Returning None.") return None edge1 = Topology.SuperTopologies(endPoints[0], wire, topologyType="edge")[0] sv = Edge.StartVertex(edge1) if (Topology.IsSame(endPoints[0], sv)): wireStartVertex = endPoints[0] wireEndVertex = endPoints[1] else: wireStartVertex = endPoints[1] wireEndVertex = endPoints[0] return [wireStartVertex, wireEndVertex]
def StartVertex(wire)
-
Returns the start vertex of the input wire. The wire must be manifold and open.
Expand source code
@staticmethod def StartVertex(wire): """ Returns the start vertex of the input wire. The wire must be manifold and open. """ sv, ev = Wire.StartEndVertices(wire) return sv
def Trapezoid(origin=None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a trapezoid.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the trapezoid. The default is None which results in the trapezoid being placed at (0, 0, 0).
widthA
:float
, optional- The width of the bottom edge of the trapezoid. The default is 1.0.
widthB
:float
, optional- The width of the top edge of the trapezoid. The default is 0.75.
offsetA
:float
, optional- The offset of the bottom edge of the trapezoid. The default is 0.0.
offsetB
:float
, optional- The offset of the top edge of the trapezoid. The default is 0.0.
length
:float
, optional- The length of the trapezoid. The default is 1.0.
direction
:list
, optional- The vector representing the up direction of the trapezoid. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the trapezoid. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
- The created trapezoid.
Expand source code
@staticmethod def Trapezoid(origin= None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a trapezoid. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the trapezoid. The default is None which results in the trapezoid being placed at (0, 0, 0). widthA : float , optional The width of the bottom edge of the trapezoid. The default is 1.0. widthB : float , optional The width of the top edge of the trapezoid. The default is 0.75. offsetA : float , optional The offset of the bottom edge of the trapezoid. The default is 0.0. offsetB : float , optional The offset of the top edge of the trapezoid. The default is 0.0. length : float , optional The length of the trapezoid. The default is 1.0. direction : list , optional The vector representing the up direction of the trapezoid. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the trapezoid. This can be "center", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire The created trapezoid. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None widthA = abs(widthA) widthB = abs(widthB) length = abs(length) if widthA < tolerance or widthB < tolerance or length < tolerance: return None if not placement.lower() in ["center", "lowerleft", "upperleft", "lowerright", "upperright"]: return None xOffset = 0 yOffset = 0 if placement.lower() == "center": xOffset = -((-widthA*0.5 + offsetA) + (-widthB*0.5 + offsetB) + (widthA*0.5 + offsetA) + (widthB*0.5 + offsetB))/4.0 yOffset = 0 elif placement.lower() == "lowerleft": xOffset = -(min((-widthA*0.5 + offsetA), (-widthB*0.5 + offsetB))) yOffset = length*0.5 elif placement.lower() == "upperleft": xOffset = -(min((-widthA*0.5 + offsetA), (-widthB*0.5 + offsetB))) yOffset = -length*0.5 elif placement.lower() == "lowerright": xOffset = -(max((widthA*0.5 + offsetA), (widthB*0.5 + offsetB))) yOffset = length*0.5 elif placement.lower() == "upperright": xOffset = -(max((widthA*0.5 + offsetA), (widthB*0.5 + offsetB))) yOffset = -length*0.5 vb1 = Vertex.ByCoordinates(origin.X()-widthA*0.5+offsetA+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb2 = Vertex.ByCoordinates(origin.X()+widthA*0.5+offsetA+xOffset,origin.Y()-length*0.5+yOffset,origin.Z()) vb3 = Vertex.ByCoordinates(origin.X()+widthB*0.5+offsetB+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) vb4 = Vertex.ByCoordinates(origin.X()-widthB*0.5++offsetB+xOffset,origin.Y()+length*0.5+yOffset,origin.Z()) baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], True) if direction != [0, 0, 1]: baseWire = Topology.Orient(baseWire, origin=origin, dirA=[0, 0, 1], dirB=direction) return baseWire
def VertexByDistance(wire, distance: float = 0.0, origin=None, tolerance=0.0001)
-
Creates a vertex along the input wire offset by the input distance from the input origin.
Parameters
edge
:topologic_core.Edge
- The input edge.
distance
:float
, optional- The offset distance. The default is 0.
origin
:topologic_core.Vertex
, optional- The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input edge. The default is None.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Vertex
- The created vertex.
Expand source code
@staticmethod def VertexByDistance(wire, distance: float = 0.0, origin= None, tolerance = 0.0001): """ Creates a vertex along the input wire offset by the input distance from the input origin. Parameters ---------- edge : topologic_core.Edge The input edge. distance : float , optional The offset distance. The default is 0. origin : topologic_core.Vertex , optional The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input edge. The default is None. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Vertex The created vertex. """ from topologicpy.Vertex import Vertex def compute_u(u): def count_decimal_places(number): try: # Convert the number to a string to analyze decimal places num_str = str(number) # Split the number into integer and decimal parts integer_part, decimal_part = num_str.split('.') # Return the length of the decimal part return len(decimal_part) except ValueError: # If there's no decimal part, return 0 return 0 dp = count_decimal_places(u) u = -(int(u) - u) return round(u,dp) if not Topology.IsInstance(wire, "Wire"): print("Wire.VertexByDistance - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None wire_length = Wire.Length(wire) if wire_length < tolerance: print("Wire.VertexByDistance: The input wire parameter is a degenerate topologic wire. Returning None.") return None if abs(distance) < tolerance: return Wire.StartVertex(wire) if abs(distance - wire_length) < tolerance: return Wire.EndVertex(wire) if not Wire.IsManifold(wire): print("Wire.VertexAtParameter - Error: The input wire parameter is non-manifold. Returning None.") return None if origin == None: origin = Wire.StartVertex(wire) if not Topology.IsInstance(origin, "Vertex"): print("Wire.VertexByDistance - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None if not Vertex.IsInternal(origin, wire, tolerance=tolerance): print("Wire.VertexByDistance - Error: The input origin parameter is not internal to the input wire parameter. Returning None.") return None if Vertex.Distance(Wire.StartVertex(wire), origin) < tolerance: u = distance/wire_length elif Vertex.Distance(Wire.EndVertex(wire), origin) < tolerance: u = 1 - distance/wire_length else: d = Wire.VertexDistance(wire, origin) + distance u = d/wire_length return Wire.VertexByParameter(wire, u=compute_u(u))
def VertexByParameter(wire, u: float = 0)
-
Creates a vertex along the input wire offset by the input u parameter. The wire must be manifold.
Parameters
wire
:topologic_core.Wire
- The input wire.
u
:float
, optional- The u parameter along the input topologic Wire. A parameter of 0 returns the start vertex. A parameter of 1 returns the end vertex. The default is 0.
Returns
topologic_core.Vertex
- The vertex at the input u parameter
Expand source code
@staticmethod def VertexByParameter(wire, u: float = 0): """ Creates a vertex along the input wire offset by the input *u* parameter. The wire must be manifold. Parameters ---------- wire : topologic_core.Wire The input wire. u : float , optional The *u* parameter along the input topologic Wire. A parameter of 0 returns the start vertex. A parameter of 1 returns the end vertex. The default is 0. Returns ------- topologic_core.Vertex The vertex at the input u parameter """ from topologicpy.Edge import Edge if not Topology.IsInstance(wire, "Wire"): print("Wire.VertexAtParameter - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if u < 0 or u > 1: print("Wire.VertexAtParameter - Error: The input u parameter is not within the valid range of [0, 1]. Returning None.") return None if not Wire.IsManifold(wire): print("Wire.VertexAtParameter - Error: The input wire parameter is non-manifold. Returning None.") return None if u == 0: return Wire.StartVertex(wire) if u == 1: return Wire.EndVertex(wire) edges = Wire.Edges(wire) total_length = 0.0 edge_lengths = [] # Compute the total length of the wire for edge in edges: e_length = Edge.Length(edge) edge_lengths.append(e_length) total_length += e_length # Initialize variables for tracking the current edge and accumulated length current_edge = None accumulated_length = 0.0 # Iterate over the lines to find the appropriate segment for i, edge in enumerate(edges): edge_length = edge_lengths[i] # Check if the desired point is on this line if u * total_length <= accumulated_length + edge_length: current_edge = edge break else: accumulated_length += edge_length # Calculate the residual u value for the current line residual_u = (u * total_length - accumulated_length) / Edge.Length(current_edge) # Compute the point at the parameter on the current line vertex = Edge.VertexByParameter(current_edge, residual_u) return vertex
def VertexDistance(wire, vertex, origin=None, mantissa: int = 6, tolerance: float = 0.0001)
-
Returns the distance, computed along the input wire of the input vertex from the input origin vertex.
Parameters
wire
:topologic_core.Wire
- The input wire.
vertex
:topologic_core.Vertex
- The input vertex
origin
:topologic_core.Vertex
, optional- The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input wire. The default is None.
mantissa
:int
, optional- The desired length of the mantissa. The default is 6.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
float
- The distance of the input vertex from the input origin along the input wire.
Expand source code
@staticmethod def VertexDistance(wire, vertex, origin= None, mantissa: int = 6, tolerance: float = 0.0001): """ Returns the distance, computed along the input wire of the input vertex from the input origin vertex. Parameters ---------- wire : topologic_core.Wire The input wire. vertex : topologic_core.Vertex The input vertex origin : topologic_core.Vertex , optional The origin of the offset distance. If set to None, the origin will be set to the start vertex of the input wire. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- float The distance of the input vertex from the input origin along the input wire. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Topology import Topology if not Topology.IsInstance(wire, "Wire"): print("Wire.VertexDistance - Error: The input wire parameter is not a valid topologic wire. Returning None.") return None if not Topology.IsInstance(vertex, "Vertex"): print("Wire.VertexDistance - Error: The input vertex parameter is not a valid topologic vertex. Returning None.") return None wire_length = Wire.Length(wire) if wire_length < tolerance: print("Wire.VertexDistance: The input wire parameter is a degenerate topologic wire. Returning None.") return None if origin == None: origin = Wire.StartVertex(wire) if not Topology.IsInstance(origin, "Vertex"): print("Wire.VertexDistance - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None if not Vertex.IsInternal(vertex, wire, tolerance=tolerance): print("Wire.VertexDistance: The input vertex parameter is not internal to the input wire parameter. Returning None.") return None def distance_from_start(wire, v): total_distance = 0.0 found = False # Iterate over the edges of the wire for edge in Wire.Edges(wire): if Vertex.IsInternal(v, edge, tolerance=tolerance): total_distance += Vertex.Distance(Edge.StartVertex(edge), v) found = True break total_distance += Edge.Length(edge) if found == False: return None return total_distance d1 = distance_from_start(wire, vertex) d2 = distance_from_start(wire, origin) if d1 == None: print("Wire.VertexDistance - Error: The input vertex parameter is not internal to the input wire parameter. Returning None.") return None if d2 == None: print("Wire.VertexDistance - Error: The input origin parameter is not internal to the input wire parameter. Returning None.") return None return round(abs(d2-d1), mantissa)
def Vertices(wire) ‑> list
-
Returns the list of vertices of the input wire.
Parameters
wire
:topologic_core.Wire
- The input wire.
Returns
list
- The list of vertices.
Expand source code
@staticmethod def Vertices(wire) -> list: """ Returns the list of vertices of the input wire. Parameters ---------- wire : topologic_core.Wire The input wire. Returns ------- list The list of vertices. """ if not Topology.IsInstance(wire, "Wire"): return None vertices = [] _ = wire.Vertices(None, vertices) return vertices