Module Shell
Expand source code
# Copyright (C) 2024
# Wassim Jabi <wassim.jabi@gmail.com>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/>.
import topologic_core as topologic
import math
import os
import warnings
try:
from tqdm.auto import tqdm
except:
print("Shell - Installing required tqdm library.")
try:
os.system("pip install tqdm")
except:
os.system("pip install tqdm --user")
try:
from tqdm.auto import tqdm
print("Shell - tqdm library installed correctly.")
except:
warnings.warn("Shell - Error: Could not import tqdm.")
try:
from scipy.spatial import Delaunay
from scipy.spatial import Voronoi
except:
print("Shell - Install required scipy library.")
try:
os.system("pip install scipy")
except:
os.system("pip install scipy --user")
try:
from scipy.spatial import Delaunay
from scipy.spatial import Voronoi
except:
warnings.warn("Shell - Error: Could not import scipy.")
class Shell():
@staticmethod
def ByDisjointFaces(externalBoundary, faces, maximumGap=0.5, mergeJunctions=False, threshold=0.5, uSides=1, vSides=1, transferDictionaries=False, tolerance=0.0001):
"""
Creates a shell from an input list of disjointed faces. THIS IS STILL EXPERIMENTAL
Parameters
----------
externalBoundary : topologic_core.Face
The input external boundary of the faces. This resembles a ribbon (face with hole) where its interior boundary touches the edges of the input list of faces.
faces : list
The input list of faces.
maximumGap : float , optional
The length of the maximum gap between the faces. The default is 0.5.
mergeJunctions : bool , optional
If set to True, the interior junctions are merged into a single vertex. Otherwise, diagonal edges are added to resolve transitions between different gap distances.
threshold : float , optional
The desired threshold under which vertices are merged into a single vertex. The default is 0.5.
uSides : int , optional
The desired number of sides along the X axis for the grid that subdivides the input faces to aid in processing. The default is 1.
vSides : int , optional
The desired number of sides along the Y axis for the grid that subdivides the input faces to aid in processing. The default is 1.
transferDictionaries : bool, optional.
If set to True, the dictionaries in the input list of faces are transfered to the faces of the resulting shell. The default is False.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Shell
The created Shell.
"""
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.Helper import Helper
from topologicpy.Topology import Topology
from topologicpy.Grid import Grid
from topologicpy.Dictionary import Dictionary
def removeShards(edges, hostTopology, maximumGap=0.5):
returnEdges = []
for e in tqdm(edges, desc="Removing Shards", leave=False):
if Edge.Length(e) < maximumGap:
sv = Edge.StartVertex(e)
ev = Edge.EndVertex(e)
sEdges = Topology.SuperTopologies(sv, hostTopology, "edge")
sn = len(sEdges)
eEdges = Topology.SuperTopologies(ev, hostTopology, "edge")
en = len(eEdges)
if sn >= 2 and en >= 2:
returnEdges.append(e)
else:
returnEdges.append(e)
return returnEdges
def extendEdges(edges, hostTopology, maximumGap=0.5):
returnEdges = []
for e in tqdm(edges, desc="Extending Edges", leave=False):
sv = Edge.StartVertex(e)
ev = Edge.EndVertex(e)
sEdges = Topology.SuperTopologies(sv, hostTopology, "edge")
sn = len(sEdges)
eEdges = Topology.SuperTopologies(ev, hostTopology, "edge")
en = len(eEdges)
if sn == 1:
ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=True)
returnEdges.append(ee)
elif en == 1:
ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=False)
returnEdges.append(ee)
else:
returnEdges.append(e)
return returnEdges
facesCluster = Cluster.ByTopologies(faces)
internalBoundary = Face.ByWire(Face.InternalBoundaries(externalBoundary)[0], tolerance=tolerance)
bb = Topology.BoundingBox(internalBoundary)
bb_d = Topology.Dictionary(bb)
unitU = Dictionary.ValueAtKey(bb_d, 'width') / uSides
unitV = Dictionary.ValueAtKey(bb_d, 'length') / vSides
uRange = [u*unitU for u in range(uSides)]
vRange = [v*unitV for v in range(vSides)]
grid = Grid.EdgesByDistances(internalBoundary, uRange=uRange, vRange=vRange, clip=True)
grid = Topology.Slice(internalBoundary, grid, tolerance=tolerance)
grid_faces = Topology.Faces(grid)
skeletons = []
for ib in tqdm(grid_faces, desc="Processing "+str(len(grid_faces))+" tiles", leave=False):
building_shell = Topology.Slice(ib, facesCluster, tolerance=tolerance)
wall_faces = Topology.Faces(building_shell)
walls = []
for w1 in wall_faces:
iv = Topology.InternalVertex(w1, tolerance=tolerance)
flag = False
for w2 in faces:
if Vertex.IsInternal(iv, w2):
flag = True
break;
if flag == False:
walls.append(w1)
for wall in walls:
skeleton = Wire.Skeleton(wall, tolerance=0.001) # This tolerance works better.
skeleton = Topology.Difference(skeleton, facesCluster, tolerance=tolerance)
skeleton = Topology.Difference(skeleton, Face.Wire(wall), tolerance=tolerance)
skeletons.append(skeleton)
if len(skeletons) > 0:
skeleton_cluster = Cluster.ByTopologies(skeletons+[internalBoundary])
skEdges = Topology.SelfMerge(Cluster.ByTopologies(removeShards(Topology.Edges(skeleton_cluster), skeleton_cluster, maximumGap=maximumGap)), tolerance=tolerance)
if Topology.IsInstance(skEdges, "Edge"):
skEdges = extendEdges([skEdges], skEdges, maximumGap=maximumGap)
else:
skEdges = extendEdges(Topology.Edges(skEdges), skEdges, maximumGap=maximumGap)
if len(skEdges) < 1:
print("ShellByDisjointFaces - Warning: No edges were extended.")
#return Cluster.ByTopologies(skEdges)
#print("ShellByDisjointFaces - Error: Could not derive central skeleton of interior walls. Returning None.")
#return None
shell = Topology.Slice(internalBoundary, skeleton_cluster, tolerance=tolerance)
if mergeJunctions == True:
vertices = Shell.Vertices(shell)
centers = []
used = []
for v in vertices:
for w in vertices:
if not Topology.IsSame(v, w) and not w in used:
if Vertex.Distance(v, w) < threshold:
centers.append(v)
used.append(w)
edges = Shell.Edges(shell)
new_edges = []
for e in edges:
sv = Edge.StartVertex(e)
ev = Edge.EndVertex(e)
for v in centers:
if Vertex.Distance(sv, v) < threshold:
sv = v
if Vertex.Distance(ev, v) < threshold:
ev = v
new_edges.append(Edge.ByVertices([sv,ev], tolerance=tolerance))
cluster = Cluster.ByTopologies(new_edges)
vertices = Topology.Vertices(cluster)
edges = Topology.Edges(shell)
xList = list(set([Vertex.X(v) for v in vertices]))
xList.sort()
xList = Helper.MergeByThreshold(xList, 0.5)
yList = list(set([Vertex.Y(v) for v in vertices]))
yList.sort()
yList = Helper.MergeByThreshold(yList, 0.5)
yList.sort()
centers = []
new_edges = []
for e in edges:
sv = Edge.StartVertex(e)
ev = Edge.EndVertex(e)
svx = Vertex.X(sv)
svy = Vertex.Y(sv)
evx = Vertex.X(ev)
evy = Vertex.Y(ev)
for x in xList:
if abs(svx-x) < threshold:
svx = x
break;
for y in yList:
if abs(svy-y) < threshold:
svy = y
break;
sv = Vertex.ByCoordinates(svx, svy, 0)
for x in xList:
if abs(evx-x) < threshold:
evx = x
break;
for y in yList:
if abs(evy-y) < threshold:
evy = y
break;
sv = Vertex.ByCoordinates(svx, svy, 0)
ev = Vertex.ByCoordinates(evx, evy, 0)
new_edges.append(Edge.ByVertices([sv, ev], tolerance=tolerance))
cluster = Cluster.ByTopologies(new_edges)
eb = Face.ByWire(Shell.ExternalBoundary(shell), tolerance=tolerance)
shell = Topology.Slice(eb, cluster, tolerance=tolerance)
if not Topology.IsInstance(shell, "Shell"):
try:
temp_wires = [Wire.RemoveCollinearEdges(w, angTolerance=1.0) for w in Topology.Wires(shell)]
temp_faces = [Face.ByWire(w, tolerance=tolerance) for w in temp_wires]
except:
temp_faces = Topology.Faces(shell)
shell = Shell.ByFaces(temp_faces, tolerance=tolerance)
if transferDictionaries == True:
selectors = []
for f in faces:
d = Topology.Dictionary(f)
s = Topology.InternalVertex(f, tolerance=tolerance)
s = Topology.SetDictionary(s, d)
selectors.append(s)
_ = Topology.TransferDictionariesBySelectors(topology=shell, selectors=selectors, tranFaces=True, tolerance=tolerance)
return shell
return None
@staticmethod
def ByFaces(faces: list, tolerance: float = 0.0001):
"""
Creates a shell from the input list of faces.
Parameters
----------
faces : list
The input list of faces.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Shell
The created Shell.
"""
from topologicpy.Topology import Topology
if not isinstance(faces, list):
return None
faceList = [x for x in faces if Topology.IsInstance(x, "Face")]
if len(faceList) == 0:
print("Shell.ByFaces - Error: The input faces list does not contain any valid faces. Returning None.")
return None
shell = topologic.Shell.ByFaces(faceList, tolerance) # Hook to Core
if not Topology.IsInstance(shell, "Shell"):
shell = Topology.SelfMerge(shell, tolerance=tolerance)
if Topology.IsInstance(shell, "Shell"):
return shell
else:
print("Shell.ByFaces - Error: Could not create shell. Returning None.")
return None
else:
return shell
@staticmethod
def ByFacesCluster(cluster, tolerance: float = 0.0001):
"""
Creates a shell from the input cluster of faces.
Parameters
----------
cluster : topologic_core.Cluster
The input cluster of faces.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Shell
The created shell.
"""
from topologicpy.Topology import Topology
if not Topology.IsInstance(cluster, "Cluster"):
return None
faces = []
_ = cluster.Faces(None, faces)
return Shell.ByFaces(faces, tolerance=tolerance)
@staticmethod
def ByWires(wires: list, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False):
"""
Creates a shell by lofting through the input wires
Parameters
----------
wires : list
The input list of wires.
triangulate : bool , optional
If set to True, the faces will be triangulated. The default is True.
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.Shell
The creates shell.
"""
from topologicpy.Edge import Edge
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Topology import Topology
if not isinstance(wires, list):
return None
wireList = [x for x in wires if Topology.IsInstance(x, "Wire")]
faces = []
for i in range(len(wireList)-1):
wire1 = wireList[i]
wire2 = wireList[i+1]
if Topology.Type(wire1) < Topology.TypeID("Edge") or Topology.Type(wire2) < Topology.TypeID("Edge"):
return None
if Topology.Type(wire1) == Topology.TypeID("Edge"):
w1_edges = [wire1]
else:
w1_edges = Topology.Edges(wire1)
if Topology.Type(wire2) == Topology.TypeID("Edge"):
w2_edges = [wire2]
else:
w2_edges = Topology.Edges(wire2)
if len(w1_edges) != len(w2_edges):
return None
if triangulate == True:
for j in range (len(w1_edges)):
e1 = w1_edges[j]
e2 = w2_edges[j]
e3 = None
e4 = None
try:
e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
except:
e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e4], tolerance=tolerance), tolerance=tolerance))
try:
e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
except:
e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e3],tolerance=tolerance), tolerance=tolerance))
if e3 and e4:
e5 = Edge.ByVertices([e1.StartVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
faces.append(Face.ByWire(Wire.ByEdges([e1, e5, e4], tolerance=tolerance), tolerance=tolerance))
faces.append(Face.ByWire(Wire.ByEdges([e2, e5, e3], tolerance=tolerance), tolerance=tolerance))
else:
for j in range (len(w1_edges)):
e1 = w1_edges[j]
e2 = w2_edges[j]
e3 = None
e4 = None
try:
e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
except:
try:
e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
except:
pass
try:
e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
except:
try:
e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
except:
pass
if e3 and e4:
try:
faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2, e3], tolerance=tolerance), tolerance=tolerance))
except:
faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2, e4], tolerance=tolerance), tolerance=tolerance))
elif e3:
faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2], tolerance=tolerance), tolerance=tolerance))
elif e4:
faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2], tolerance=tolerance), tolerance=tolerance))
return Shell.ByFaces(faces, tolerance=tolerance)
@staticmethod
def ByWiresCluster(cluster, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False):
"""
Creates a shell by lofting through the input cluster of wires
Parameters
----------
wires : topologic_core.Cluster
The input cluster of wires.
triangulate : bool , optional
If set to True, the faces will be triangulated. The default is True.
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.Shell
The creates shell.
"""
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not cluster:
return None
if not Topology.IsInstance(cluster, "Cluster"):
return None
wires = Cluster.Wires(cluster)
return Shell.ByWires(wires, triangulate=triangulate, tolerance=tolerance, silent=silent)
@staticmethod
def Circle(origin= None, radius: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, 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 32.
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.
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 pie. 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.Shell
The created circle.
"""
return Shell.Pie(origin=origin, radiusA=radius, radiusB=0, sides=sides, rings=1, fromAngle=fromAngle, toAngle=toAngle, direction=direction, placement=placement, tolerance=tolerance)
@staticmethod
def Delaunay(vertices: list, face= None, tolerance: float = 0.0001):
"""
Returns a delaunay partitioning of the input vertices. The vertices must be coplanar. See https://en.wikipedia.org/wiki/Delaunay_triangulation.
Parameters
----------
vertices : list
The input list of vertices.
face : topologic_core.Face , optional
The input face. If specified, the delaunay triangulation is clipped to the face.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
shell
A shell representing the delaunay triangulation of the input vertices.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Dictionary import Dictionary
from random import sample
from scipy.spatial import Delaunay as SCIDelaunay
import numpy as np
if not isinstance(vertices, list):
return None
vertices = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
if len(vertices) < 3:
return None
# Create a Vertex at the world's origin (0, 0, 0)
world_origin = Vertex.Origin()
if Topology.IsInstance(face, "Face"):
# Flatten the face
origin = Topology.Centroid(face)
normal = Face.Normal(face)
flatFace = Topology.Flatten(face, origin=origin, direction=normal)
faceVertices = Face.Vertices(face)
vertices += faceVertices
# Create a cluster of the input vertices
verticesCluster = Cluster.ByTopologies(vertices)
# Flatten the cluster using the same transformations
verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal)
vertices = Cluster.Vertices(verticesCluster)
points = []
for v in vertices:
points.append([Vertex.X(v), Vertex.Y(v)])
delaunay = SCIDelaunay(points)
simplices = delaunay.simplices
faces = []
for simplex in simplices:
tempTriangleVertices = []
tempTriangleVertices.append(vertices[simplex[0]])
tempTriangleVertices.append(vertices[simplex[1]])
tempTriangleVertices.append(vertices[simplex[2]])
tempFace = Face.ByWire(Wire.ByVertices(tempTriangleVertices), tolerance=tolerance)
faces.append(tempFace)
shell = Shell.ByFaces(faces, tolerance=tolerance)
if shell == None:
shell = Cluster.ByTopologies(faces)
if Topology.IsInstance(face, "Face"):
edges = Topology.Edges(shell)
shell = Topology.Slice(flatFace, Cluster.ByTopologies(edges))
# Get the internal boundaries of the face
wires = Face.InternalBoundaries(flatFace)
ibList = []
if len(wires) > 0:
ibList = [Face.ByWire(w) for w in wires]
cluster = Cluster.ByTopologies(ibList)
shell = Topology.Difference(shell, cluster)
shell = Topology.Unflatten(shell, origin=origin, direction=normal)
return shell
@staticmethod
def Edges(shell) -> list:
"""
Returns the edges of the input shell.
Parameters
----------
shell : topologic_core.Shell
The input shell.
Returns
-------
list
The list of edges.
"""
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
return None
edges = []
_ = shell.Edges(None, edges)
return edges
@staticmethod
def ExternalBoundary(shell, tolerance: float = 0.0001):
"""
Returns the external boundary of the input shell.
Parameters
----------
shell : topologic_core.Shell
The input shell.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Wire or topologic_core.Cluster
The external boundary of the input shell. If the shell has holes, the return value will be a cluster of wires.
"""
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
return None
edges = []
_ = shell.Edges(None, edges)
obEdges = []
for anEdge in edges:
faces = []
_ = anEdge.Faces(shell, faces)
if len(faces) == 1:
obEdges.append(anEdge)
return Topology.SelfMerge(Cluster.ByTopologies(obEdges), tolerance=tolerance)
@staticmethod
def Faces(shell) -> list:
"""
Returns the faces of the input shell.
Parameters
----------
shell : topologic_core.Shell
The input shell.
Returns
-------
list
The list of faces.
"""
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
return None
faces = []
_ = shell.Faces(None, faces)
return faces
@staticmethod
def IsOnBoundary(shell, vertex, tolerance: float = 0.0001) -> bool:
"""
Returns True if the input vertex is on the boundary of the input shell. Returns False otherwise. On the boundary is defined as being on the boundary of one of the shell's external or internal boundaries
Parameters
----------
shell : topologic_core.Shell
The input shell.
vertex : topologic_core.Vertex
The input vertex.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
bool
Returns True if the input vertex is inside the input shell. Returns False otherwise.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
return None
if not Topology.IsInstance(vertex, "Vertex"):
return None
boundary = Shell.ExternalBoundary(shell, tolerance=tolerance)
if Vertex.IsInternal(vertex, boundary, tolerance=tolerance):
return True
internal_boundaries = Shell.InternalBoundaries(shell, tolerance=tolerance)
for ib in internal_boundaries:
if Vertex.IsInternal(vertex, ib, tolerance=tolerance):
return True
return False
@staticmethod
def HyperbolicParaboloidRectangularDomain(origin= None, llVertex= None, lrVertex= None, ulVertex= None, urVertex= None,
uSides: int = 10, vSides: int = 10, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates a hyperbolic paraboloid with a rectangular domain.
Parameters
----------
origin : topologic_core.Vertex , optional
The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
llVertex : topologic_core.Vertex , optional
The lower left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, -0.5, -0.5).
lrVertex : topologic_core.Vertex , optional
The lower right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, -0.5, 0.5).
ulVertex : topologic_core.Vertex , optional
The upper left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, 0.5, 0.5).
urVertex : topologic_core.Vertex , optional
The upper right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, 0.5, -0.5).
uSides : int , optional
The number of segments along the X axis. The default is 10.
vSides : int , optional
The number of segments along the Y axis. The default is 10.
direction : list , optional
The vector representing the up direction of the hyperbolic parabolid. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the hyperbolic parabolid. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Shell
The created hyperbolic paraboloid.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Edge import Edge
from topologicpy.Face import Face
from topologicpy.Topology import Topology
if not Topology.IsInstance(origin, "Vertex"):
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(llVertex, "Vertex"):
llVertex = Vertex.ByCoordinates(-0.5, -0.5, -0.5)
if not Topology.IsInstance(lrVertex, "Vertex"):
lrVertex = Vertex.ByCoordinates(0.5, -0.5, 0.5)
if not Topology.IsInstance(ulVertex, "Vertex"):
ulVertex = Vertex.ByCoordinates(-0.5, 0.5, 0.5)
if not Topology.IsInstance(urVertex, "Vertex"):
urVertex = Vertex.ByCoordinates(0.5, 0.5, -0.5)
e1 = Edge.ByVertices([llVertex, lrVertex], tolerance=tolerance)
e3 = Edge.ByVertices([urVertex, ulVertex], tolerance=tolerance)
edges = []
for i in range(uSides+1):
v1 = Edge.VertexByParameter(e1, float(i)/float(uSides))
v2 = Edge.VertexByParameter(e3, 1.0 - float(i)/float(uSides))
edges.append(Edge.ByVertices([v1, v2], tolerance=tolerance))
faces = []
for i in range(uSides):
for j in range(vSides):
v1 = Edge.VertexByParameter(edges[i], float(j)/float(vSides))
v2 = Edge.VertexByParameter(edges[i], float(j+1)/float(vSides))
v3 = Edge.VertexByParameter(edges[i+1], float(j+1)/float(vSides))
v4 = Edge.VertexByParameter(edges[i+1], float(j)/float(vSides))
faces.append(Face.ByVertices([v1, v2, v4]))
faces.append(Face.ByVertices([v4, v2, v3]))
returnTopology = Shell.ByFaces(faces, tolerance=tolerance)
if not returnTopology:
returnTopology = None
xOffset = 0
yOffset = 0
zOffset = 0
minX = min([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()])
maxX = max([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()])
minY = min([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()])
maxY = max([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()])
minZ = min([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()])
maxZ = max([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()])
if placement.lower() == "lowerleft":
xOffset = -minX
yOffset = -minY
zOffset = -minZ
elif placement.lower() == "bottom":
xOffset = -(minX + (maxX - minX)*0.5)
yOffset = -(minY + (maxY - minY)*0.5)
zOffset = -minZ
elif placement.lower() == "center":
xOffset = -(minX + (maxX - minX)*0.5)
yOffset = -(minY + (maxY - minY)*0.5)
zOffset = -(minZ + (maxZ - minZ)*0.5)
returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset)
returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin)
returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction)
return returnTopology
@staticmethod
def HyperbolicParaboloidCircularDomain(origin= None, radius: float = 0.5, sides: int = 36, rings: int = 10,
A: float = 2.0, B: float = -2.0, direction: list = [0, 0, 1],
placement: str = "center", tolerance: float = 0.0001):
"""
Creates a hyperbolic paraboloid with a circular domain. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
Parameters
----------
origin : topologic_core.Vertex , optional
The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
radius : float , optional
The desired radius of the hyperbolic paraboloid. The default is 0.5.
sides : int , optional
The desired number of sides of the hyperbolic parabolid. The default is 36.
rings : int , optional
The desired number of concentric rings of the hyperbolic parabolid. The default is 10.
A : float , optional
The *A* constant in the equation z = A*x^2^ + B*y^2^. The default is 2.0.
B : float , optional
The *B* constant in the equation z = A*x^2^ + B*y^2^. The default is -2.0.
direction : list , optional
The vector representing the up direction of the hyperbolic paraboloid. 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", "bottom". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Shell
The created hyperboloic paraboloid.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not Topology.IsInstance(origin, "Vertex"):
origin = Vertex.ByCoordinates(0, 0, 0)
uOffset = float(360)/float(sides)
vOffset = float(radius)/float(rings)
faces = []
for i in range(rings-1):
r1 = radius - vOffset*i
r2 = radius - vOffset*(i+1)
for j in range(sides-1):
a1 = math.radians(uOffset)*j
a2 = math.radians(uOffset)*(j+1)
x1 = math.sin(a1)*r1
y1 = math.cos(a1)*r1
z1 = A*x1*x1 + B*y1*y1
x2 = math.sin(a1)*r2
y2 = math.cos(a1)*r2
z2 = A*x2*x2 + B*y2*y2
x3 = math.sin(a2)*r2
y3 = math.cos(a2)*r2
z3 = A*x3*x3 + B*y3*y3
x4 = math.sin(a2)*r1
y4 = math.cos(a2)*r1
z4 = A*x4*x4 + B*y4*y4
v1 = Vertex.ByCoordinates(x1,y1,z1)
v2 = Vertex.ByCoordinates(x2,y2,z2)
v3 = Vertex.ByCoordinates(x3,y3,z3)
v4 = Vertex.ByCoordinates(x4,y4,z4)
f1 = Face.ByVertices([v1,v2,v4])
f2 = Face.ByVertices([v4,v2,v3])
faces.append(f1)
faces.append(f2)
a1 = math.radians(uOffset)*(sides-1)
a2 = math.radians(360)
x1 = math.sin(a1)*r1
y1 = math.cos(a1)*r1
z1 = A*x1*x1 + B*y1*y1
x2 = math.sin(a1)*r2
y2 = math.cos(a1)*r2
z2 = A*x2*x2 + B*y2*y2
x3 = math.sin(a2)*r2
y3 = math.cos(a2)*r2
z3 = A*x3*x3 + B*y3*y3
x4 = math.sin(a2)*r1
y4 = math.cos(a2)*r1
z4 = A*x4*x4 + B*y4*y4
v1 = Vertex.ByCoordinates(x1,y1,z1)
v2 = Vertex.ByCoordinates(x2,y2,z2)
v3 = Vertex.ByCoordinates(x3,y3,z3)
v4 = Vertex.ByCoordinates(x4,y4,z4)
f1 = Face.ByVertices([v1,v2,v4])
f2 = Face.ByVertices([v4,v2,v3])
faces.append(f1)
faces.append(f2)
# Special Case: Center triangles
r = vOffset
x1 = 0
y1 = 0
z1 = 0
v1 = Vertex.ByCoordinates(x1,y1,z1)
for j in range(sides-1):
a1 = math.radians(uOffset)*j
a2 = math.radians(uOffset)*(j+1)
x2 = math.sin(a1)*r
y2 = math.cos(a1)*r
z2 = A*x2*x2 + B*y2*y2
#z2 = 0
x3 = math.sin(a2)*r
y3 = math.cos(a2)*r
z3 = A*x3*x3 + B*y3*y3
#z3 = 0
v2 = Vertex.ByCoordinates(x2,y2,z2)
v3 = Vertex.ByCoordinates(x3,y3,z3)
f1 = Face.ByVertices([v2,v1,v3])
faces.append(f1)
a1 = math.radians(uOffset)*(sides-1)
a2 = math.radians(360)
x2 = math.sin(a1)*r
y2 = math.cos(a1)*r
z2 = A*x2*x2 + B*y2*y2
x3 = math.sin(a2)*r
y3 = math.cos(a2)*r
z3 = A*x3*x3 + B*y3*y3
v2 = Vertex.ByCoordinates(x2,y2,z2)
v3 = Vertex.ByCoordinates(x3,y3,z3)
f1 = Face.ByVertices([v2,v1,v3])
faces.append(f1)
returnTopology = Shell.ByFaces(faces, tolerance)
if not returnTopology:
returnTopology = Cluster.ByTopologies(faces)
vertices = []
_ = returnTopology.Vertices(None, vertices)
xList = []
yList = []
zList = []
for aVertex in vertices:
xList.append(aVertex.X())
yList.append(aVertex.Y())
zList.append(aVertex.Z())
minX = min(xList)
maxX = max(xList)
minY = min(yList)
maxY = max(yList)
minZ = min(zList)
maxZ = max(zList)
xOffset = 0
yOffset = 0
zOffset = 0
if placement.lower() == "lowerleft":
xOffset = -minX
yOffset = -minY
zOffset = -minZ
elif placement.lower() == "bottom":
xOffset = -(minX + (maxX - minX)*0.5)
yOffset = -(minY + (maxY - minY)*0.5)
zOffset = -minZ
elif placement.lower() == "center":
xOffset = -(minX + (maxX - minX)*0.5)
yOffset = -(minY + (maxY - minY)*0.5)
zOffset = -(minZ + (maxZ - minZ)*0.5)
returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset)
returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin)
returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction)
return returnTopology
@staticmethod
def InternalBoundaries(shell, tolerance=0.0001):
"""
Returns the internal boundaries (closed wires) of the input shell. Internal boundaries are considered holes.
Parameters
----------
shell : topologic_core.Shell
The input shell.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
list
The list of internal boundaries
"""
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
edges = []
_ = shell.Edges(None, edges)
ibEdges = []
for anEdge in edges:
faces = []
_ = anEdge.Faces(shell, faces)
if len(faces) > 1:
ibEdges.append(anEdge)
returnTopology = Topology.SelfMerge(Cluster.ByTopologies(ibEdges), tolerance=tolerance)
wires = Topology.Wires(returnTopology)
return wires
@staticmethod
def IsClosed(shell) -> bool:
"""
Returns True if the input shell is closed. Returns False otherwise.
Parameters
----------
shell : topologic_core.Shell
The input shell.
Returns
-------
bool
True if the input shell is closed. False otherwise.
"""
return shell.IsClosed()
@staticmethod
def Pie(origin= None, radiusA: float = 0.5, radiusB: float = 0.0, sides: int = 32, rings: int = 1, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
"""
Creates a pie shape.
Parameters
----------
origin : topologic_core.Vertex , optional
The location of the origin of the pie. The default is None which results in the pie being placed at (0, 0, 0).
radiusA : float , optional
The outer radius of the pie. The default is 0.5.
radiusB : float , optional
The inner radius of the pie. The default is 0.25.
sides : int , optional
The number of sides of the pie. The default is 32.
rings : int , optional
The number of rings of the pie. The default is 1.
fromAngle : float , optional
The angle in degrees from which to start creating the arc of the pie. The default is 0.
toAngle : float , optional
The angle in degrees at which to end creating the arc of the pie. The default is 360.
direction : list , optional
The vector representing the up direction of the pie. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the pie. 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.Shell
The created pie.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Face import Face
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
return None
if toAngle < fromAngle:
toAngle += 360
if abs(toAngle-fromAngle) < tolerance:
return None
fromAngle = math.radians(fromAngle)
toAngle = math.radians(toAngle)
angleRange = toAngle - fromAngle
radiusA = abs(radiusA)
radiusB = abs(radiusB)
if radiusB > radiusA:
temp = radiusA
radiusA = radiusB
radiusB = temp
if abs(radiusA - radiusB) < tolerance or radiusA < tolerance:
return None
radiusRange = radiusA - radiusB
sides = int(abs(math.floor(sides)))
if sides < 3:
return None
rings = int(abs(rings))
if radiusB < tolerance:
radiusB = 0
xOffset = 0
yOffset = 0
zOffset = 0
if placement.lower() == "lowerleft":
xOffset = radiusA
yOffset = radiusA
uOffset = float(angleRange)/float(sides)
vOffset = float(radiusRange)/float(rings)
faces = []
if radiusB > tolerance:
for i in range(rings):
r1 = radiusA - vOffset*i
r2 = radiusA - vOffset*(i+1)
for j in range(sides):
a1 = fromAngle + uOffset*j
a2 = fromAngle + uOffset*(j+1)
x1 = math.sin(a1)*r1
y1 = math.cos(a1)*r1
z1 = 0
x2 = math.sin(a1)*r2
y2 = math.cos(a1)*r2
z2 = 0
x3 = math.sin(a2)*r2
y3 = math.cos(a2)*r2
z3 = 0
x4 = math.sin(a2)*r1
y4 = math.cos(a2)*r1
z4 = 0
v1 = Vertex.ByCoordinates(x1,y1,z1)
v2 = Vertex.ByCoordinates(x2,y2,z2)
v3 = Vertex.ByCoordinates(x3,y3,z3)
v4 = Vertex.ByCoordinates(x4,y4,z4)
f1 = Face.ByVertices([v1,v2,v3,v4])
faces.append(f1)
else:
x1 = 0
y1 = 0
z1 = 0
v1 = Vertex.ByCoordinates(x1,y1,z1)
for j in range(sides):
a1 = fromAngle + uOffset*j
a2 = fromAngle + uOffset*(j+1)
x2 = math.sin(a1)*radiusA
y2 = math.cos(a1)*radiusA
z2 = 0
x3 = math.sin(a2)*radiusA
y3 = math.cos(a2)*radiusA
z3 = 0
v2 = Vertex.ByCoordinates(x2,y2,z2)
v3 = Vertex.ByCoordinates(x3,y3,z3)
f1 = Face.ByVertices([v2,v1,v3])
faces.append(f1)
shell = Shell.ByFaces(faces, tolerance=tolerance)
if not shell:
return None
shell = Topology.Translate(shell, xOffset, yOffset, zOffset)
shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin)
shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction)
return shell
@staticmethod
def Planarize(shell, origin= None, mantissa: int = 6, tolerance: float = 0.0001):
"""
Returns a planarized version of the input shell.
Parameters
----------
shell : topologic_core.Shell
The input shell.
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 shell will be projected. If set to None, the centroid of the input shell will be chosen. The default is None.
mantissa : int , optional
The desired length of the mantissa. The default is 6.
Returns
-------
topologic_core.Shell
The planarized shell.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Face import Face
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
print("Shell.Planarize - Error: The input wire parameter is not a valid topologic shell. Returning None.")
return None
if origin == None:
origin = Vertex.Origin()
if not Topology.IsInstance(origin, "Vertex"):
print("Shell.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
return None
vertices = Topology.Vertices(shell)
faces = Topology.Faces(shell)
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_shell = Topology.ReplaceVertices(shell, verticesA=vertices, verticesB=new_vertices)
new_faces = Topology.Faces(new_shell)
return Topology.SelfMerge(Cluster.ByTopologies(new_faces), tolerance=tolerance)
@staticmethod
def Rectangle(origin= None, width: float = 1.0, length: float = 1.0,
uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1],
placement: str = "center", 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.
uSides : int , optional
The number of sides along the width. The default is 2.
vSides : int , optional
The number of sides along the length. The default is 2.
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", or "lowerleft". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
topologic_core.Shell
The created shell.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Topology import Topology
if not origin:
origin = Vertex.ByCoordinates(0, 0, 0)
if not Topology.IsInstance(origin, "Vertex"):
return None
uOffset = float(width)/float(uSides)
vOffset = float(length)/float(vSides)
faces = []
if placement.lower() == "center":
wOffset = width*0.5
lOffset = length*0.5
else:
wOffset = 0
lOffset = 0
for i in range(uSides):
for j in range(vSides):
rOrigin = Vertex.ByCoordinates(i*uOffset - wOffset, j*vOffset - lOffset, 0)
w = Wire.Rectangle(origin=rOrigin, width=uOffset, length=vOffset, direction=[0, 0, 1], placement="lowerleft", tolerance=tolerance)
f = Face.ByWire(w, tolerance=tolerance)
faces.append(f)
shell = Shell.ByFaces(faces, tolerance=tolerance)
shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin)
shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction)
return shell
@staticmethod
def RemoveCollinearEdges(shell, angTolerance: float = 0.1, tolerance: float = 0.0001):
"""
Removes any collinear edges in the input shell.
Parameters
----------
shell : topologic_core.Shell
The input shell.
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.Shell
The created shell without any collinear edges.
"""
from topologicpy.Face import Face
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
print("Shell.RemoveCollinearEdges - Error: The input shell parameter is not a valid shell. Returning None.")
return None
faces = Shell.Faces(shell)
clean_faces = []
for face in faces:
clean_faces.append(Face.RemoveCollinearEdges(face, angTolerance=angTolerance, tolerance=tolerance))
return Shell.ByFaces(clean_faces, tolerance=tolerance)
@staticmethod
def Roof(face, angle: float = 45, epsilon: float = 0.01, 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.
epsilon : float , optional
The desired epsilon (another form of tolerance for distance from plane). The default is 0.01. (This is set to a larger number as it was found to work better)
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.Shell
The created roof.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Shell import Shell
from topologicpy.Cell import Cell
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Dictionary import Dictionary
import topologic_core as topologic
import math
def nearest_vertex_2d(v, vertices, tolerance=0.001):
for vertex in vertices:
x2 = Vertex.X(vertex)
y2 = Vertex.Y(vertex)
temp_v = Vertex.ByCoordinates(x2, y2, Vertex.Z(v))
if Vertex.Distance(v, temp_v) <= tolerance:
return vertex
return None
if not Topology.IsInstance(face, "Face"):
return None
angle = abs(angle)
if angle >= 90-tolerance:
return None
if angle < tolerance:
return None
origin = Topology.Centroid(face)
normal = Face.Normal(face)
flat_face = Topology.Flatten(face, origin=origin, direction=normal)
roof = Wire.Roof(flat_face, angle=angle, tolerance=tolerance)
if not roof:
return None
shell = Shell.Skeleton(flat_face, tolerance=tolerance)
faces = Shell.Faces(shell)
Topology.Show(shell)
if not faces:
return None
triangles = []
for face in faces:
internalBoundaries = Face.InternalBoundaries(face)
if len(internalBoundaries) == 0:
if len(Topology.Vertices(face)) > 3:
triangles += Face.Triangulate(face, tolerance=tolerance)
else:
triangles += [face]
roof_vertices = Topology.Vertices(roof)
flat_vertices = []
for rv in roof_vertices:
flat_vertices.append(Vertex.ByCoordinates(Vertex.X(rv), Vertex.Y(rv), 0))
final_triangles = []
for triangle in triangles:
if len(Topology.Vertices(triangle)) > 3:
triangles = Face.Triangulate(triangle, tolerance=tolerance)
else:
triangles = [triangle]
final_triangles += triangles
final_faces = []
for triangle in final_triangles:
face_vertices = Topology.Vertices(triangle)
top_vertices = []
for sv in face_vertices:
temp = nearest_vertex_2d(sv, roof_vertices, tolerance=tolerance)
if temp:
top_vertices.append(temp)
else:
top_vertices.append(sv)
tri_face = Face.ByVertices(top_vertices)
final_faces.append(tri_face)
shell = Shell.ByFaces(final_faces, tolerance=tolerance)
if not shell:
shell = Cluster.ByTopologies(final_faces)
try:
shell = Topology.RemoveCoplanarFaces(shell, epsilon=epsilon, tolerance=tolerance)
except:
pass
return shell
@staticmethod
def SelfMerge(shell, angTolerance: float = 0.1, tolerance: float = 0.0001):
"""
Creates a face by merging the faces of the input shell. The shell must be planar within the input angular tolerance.
Parameters
----------
shell : topologic_core.Shell
The input shell.
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.Face
The created face.
"""
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Shell import Shell
from topologicpy.Topology import Topology
def planarizeList(wireList):
returnList = []
for aWire in wireList:
returnList.append(Wire.Planarize(aWire))
return returnList
if not Topology.IsInstance(shell, "Shell"):
return None
ext_boundary = Shell.ExternalBoundary(shell, tolerance=tolerance)
if Topology.IsInstance(ext_boundary, "Wire"):
f = Face.ByWire(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance) or Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance))
if not f:
print("FaceByPlanarShell - Error: The input Wire is not planar and could not be fixed. Returning None.")
return None
else:
return f
elif Topology.IsInstance(ext_boundary, "Cluster"):
wires = []
_ = ext_boundary.Wires(None, wires)
faces = []
areas = []
for aWire in wires:
try:
aFace = Face.ByWire(Topology.RemoveCollinearEdges(aWire, angTolerance))
except:
aFace = Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(aWire, angTolerance)))
anArea = Face.Area(aFace)
faces.append(aFace)
areas.append(anArea)
max_index = areas.index(max(areas))
ext_boundary = faces[max_index]
int_boundaries = list(set(faces) - set([ext_boundary]))
int_wires = []
for int_boundary in int_boundaries:
temp_wires = []
_ = int_boundary.Wires(None, temp_wires)
int_wires.append(Topology.RemoveCollinearEdges(temp_wires[0], angTolerance))
temp_wires = []
_ = ext_boundary.Wires(None, temp_wires)
ext_wire = Topology.RemoveCollinearEdges(temp_wires[0], angTolerance)
try:
return Face.ByWires(ext_wire, int_wires, tolerance=tolerance)
except:
return Face.ByWires(Wire.Planarize(ext_wire), planarizeList(int_wires), tolerance=tolerance)
else:
return None
def Skeleton(face, tolerance: float = 0.001):
"""
Creates a shell 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.
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.Shell
The created straight skeleton.
"""
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Topology import Topology
import topologic_core as topologic
import math
if not Topology.IsInstance(face, "Face"):
return None
roof = Wire.Skeleton(face, tolerance=tolerance)
if not roof:
return None
br = Wire.BoundingRectangle(roof) #This works even if it is a Cluster not a Wire
br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1)
bf = Face.ByWire(br, tolerance=tolerance)
large_shell = Topology.Boolean(bf, roof, operation="slice", tolerance=tolerance)
if not large_shell:
return None
faces = Topology.Faces(large_shell)
if not faces:
return None
final_faces = []
for f in faces:
internalBoundaries = Face.InternalBoundaries(f)
if len(internalBoundaries) == 0:
final_faces.append(f)
shell = Shell.ByFaces(final_faces, tolerance=tolerance)
return shell
@staticmethod
def Simplify(shell, simplifyBoundary=True, tolerance=0.0001):
"""
Simplifies the input shell 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
----------
shell : topologic_core.Shell
The input shell.
simplifyBoundary : bool , optional
If set to True, the external boundary of the shell will be simplified as well. Otherwise, it will not be simplified. The default is True.
tolerance : float , optional
The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed.
Returns
-------
topologic_core.Shell
The simplified shell.
"""
from topologicpy.Vertex import Vertex
from topologicpy.Wire import Wire
from topologicpy.Face import Face
from topologicpy.Shell import Shell
from topologicpy.Cluster import Cluster
from topologicpy.Topology import Topology
from topologicpy.Helper import Helper
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(shell, "Shell"):
print("Shell.Simplify - Error: The input shell parameter is not a valid topologic shell. Returning None.")
return None
# Get the external boundary of the shell. This can be simplified as well, but might cause issues at the end.
# At this point, it is assumed to be left as is.
all_edges = Topology.Edges(shell)
if simplifyBoundary == False:
ext_boundary = Face.ByWire(Shell.ExternalBoundary(shell, tolerance=tolerance), tolerance=tolerance)
# Get the internal edges of the shell.
i_edges = []
for edge in all_edges:
faces = Topology.SuperTopologies(edge, shell, topologyType="face")
if len(faces) > 1: # This means that the edge separates two faces so it is internal.
i_edges.append(edge)
# Creat a Wire from the internal edges
wire = Topology.SelfMerge(Cluster.ByTopologies(i_edges), tolerance=tolerance)
else:
wire = Topology.SelfMerge(Cluster.ByTopologies(all_edges), tolerance=tolerance)
# Split the wires at its junctions (where more than two edges meet at a vertex)
components = Wire.Split(wire)
separators = []
wires = []
for component in components:
if Topology.IsInstance(component, "Cluster"):
component = Topology.SelfMerge(component, tolerance=tolerance)
if Topology.IsInstance(component, "Cluster"):
separators.append(Cluster.FreeEdges(component, tolerance=tolerance))
wires.append(Cluster.FreeWires(component, tolerance=tolerance))
if Topology.IsInstance(component, "Edge"):
separators.append(component)
if Topology.IsInstance(component, "Wire"):
wires.append(component)
if Topology.IsInstance(component, "Edge"):
separators.append(component)
if Topology.IsInstance(component, "Wire"):
wires.append(component)
wires = Helper.Flatten(wires)
separators = Helper.Flatten(separators)
results = []
for w in wires:
temp_wire = Wire.ByVertices(douglas_peucker(w, tolerance), close=False)
results.append(temp_wire)
# Make a Cluster out of the results
cluster = Cluster.ByTopologies(results)
# Get all the edges of the result
edges = Topology.Edges(cluster)
# Add them to the final edges
final_edges = edges + separators
# Make a Cluster out of the final set of edges
cluster = Cluster.ByTopologies(final_edges)
if simplifyBoundary == False:
# Slice the external boundary of the shell by the cluster
final_result = Topology.Slice(ext_boundary, cluster, tolerance=tolerance)
else:
br = Wire.BoundingRectangle(shell)
br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1.5)
br = Face.ByWire(br, tolerance=tolerance)
v = Face.VertexByParameters(br, 0.1, 0.1)
result = Topology.Slice(br, cluster, tolerance=tolerance)
faces = Topology.Faces(result)
final_faces = []
for face in faces:
if not Vertex.IsInternal(v, face, tolerance=0.01):
final_faces.append(face)
final_result = Shell.ByFaces(final_faces, tolerance=tolerance)
return final_result
@staticmethod
def Vertices(shell) -> list:
"""
Returns the vertices of the input shell.
Parameters
----------
shell : topologic_core.Shell
The input shell.
Returns
-------
list
The list of vertices.
"""
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
return None
vertices = []
_ = shell.Vertices(None, vertices)
return vertices
@staticmethod
def Voronoi(vertices: list, face= None, tolerance: float = 0.0001):
"""
Returns a voronoi partitioning of the input face based on the input vertices. The vertices must be coplanar and within the face. See https://en.wikipedia.org/wiki/Voronoi_diagram.
Parameters
----------
vertices : list
The input list of vertices.
face : topologic_core.Face , optional
The input face. If the face is not set an optimised bounding rectangle of the input vertices is used instead. The default is None.
tolerance : float , optional
The desired tolerance. The default is 0.0001.
Returns
-------
shell
A shell representing the voronoi partitioning of the input face.
"""
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.Dictionary import Dictionary
if not Topology.IsInstance(face, "Face"):
cluster = Cluster.ByTopologies(vertices)
br = Wire.BoundingRectangle(cluster, optimize=5)
face = Face.ByWire(br, tolerance=tolerance)
if not isinstance(vertices, list):
return None
vertices = [x for x in vertices if Topology.IsInstance(x, "Vertex")]
if len(vertices) < 2:
return None
# Flatten the input face
origin = Topology.Centroid(face)
normal = Face.Normal(face)
flatFace = Topology.Flatten(face, origin=origin, direction=normal)
eb = Face.ExternalBoundary(flatFace)
ibList = Face.InternalBoundaries(flatFace)
temp_verts = Topology.Vertices(eb)
new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts]
eb = Wire.ByVertices(new_verts, close=True)
new_ibList = []
for ib in ibList:
temp_verts = Topology.Vertices(ib)
new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts]
new_ibList.append(Wire.ByVertices(new_verts, close=True))
flatFace = Face.ByWires(eb, new_ibList)
# Create a cluster of the input vertices
verticesCluster = Cluster.ByTopologies(vertices)
# Flatten the cluster using the same transformations
verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal)
flatVertices = Topology.Vertices(verticesCluster)
flatVertices = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in flatVertices]
points = []
for flatVertex in flatVertices:
points.append([flatVertex.X(), flatVertex.Y()])
br = Wire.BoundingRectangle(flatFace)
br_vertices = Wire.Vertices(br)
br_x = []
br_y = []
for br_v in br_vertices:
x, y = Vertex.Coordinates(br_v, outputType="xy")
br_x.append(x)
br_y.append(y)
min_x = min(br_x)
max_x = max(br_x)
min_y = min(br_y)
max_y = max(br_y)
br_width = abs(max_x - min_x)
br_length = abs(max_y - min_y)
points.append((-br_width*4, -br_length*4))
points.append((-br_width*4, br_length*4))
points.append((br_width*4, -br_length*4))
points.append((br_width*4, br_length*4))
voronoi = Voronoi(points, furthest_site=False)
voronoiVertices = []
for v in voronoi.vertices:
voronoiVertices.append(Vertex.ByCoordinates(v[0], v[1], 0))
faces = []
for region in voronoi.regions:
tempWire = []
if len(region) > 1 and not -1 in region:
for v in region:
tempWire.append(Vertex.ByCoordinates(voronoiVertices[v].X(), voronoiVertices[v].Y(), 0))
temp_verts = []
for v in tempWire:
if len(temp_verts) == 0:
temp_verts.append(v)
elif Vertex.Index(v, temp_verts) == None:
temp_verts.append(v)
tempWire = temp_verts
temp_w = Wire.ByVertices(tempWire, close=True)
faces.append(Face.ByWire(Wire.ByVertices(tempWire, close=True), tolerance=tolerance))
shell = Shell.ByFaces(faces, tolerance=tolerance)
edges = Shell.Edges(shell)
edgesCluster = Cluster.ByTopologies(edges)
shell = Topology.Slice(flatFace,edgesCluster, tolerance=tolerance)
shell = Topology.Unflatten(shell, origin=origin, direction=normal)
return shell
@staticmethod
def Wires(shell) -> list:
"""
Returns the wires of the input shell.
Parameters
----------
shell : topologic_core.Shell
The input shell.
Returns
-------
list
The list of wires.
"""
from topologicpy.Topology import Topology
if not Topology.IsInstance(shell, "Shell"):
return None
wires = []
_ = shell.Wires(None, wires)
return wires
Classes
class Shell
-
Expand source code
class Shell(): @staticmethod def ByDisjointFaces(externalBoundary, faces, maximumGap=0.5, mergeJunctions=False, threshold=0.5, uSides=1, vSides=1, transferDictionaries=False, tolerance=0.0001): """ Creates a shell from an input list of disjointed faces. THIS IS STILL EXPERIMENTAL Parameters ---------- externalBoundary : topologic_core.Face The input external boundary of the faces. This resembles a ribbon (face with hole) where its interior boundary touches the edges of the input list of faces. faces : list The input list of faces. maximumGap : float , optional The length of the maximum gap between the faces. The default is 0.5. mergeJunctions : bool , optional If set to True, the interior junctions are merged into a single vertex. Otherwise, diagonal edges are added to resolve transitions between different gap distances. threshold : float , optional The desired threshold under which vertices are merged into a single vertex. The default is 0.5. uSides : int , optional The desired number of sides along the X axis for the grid that subdivides the input faces to aid in processing. The default is 1. vSides : int , optional The desired number of sides along the Y axis for the grid that subdivides the input faces to aid in processing. The default is 1. transferDictionaries : bool, optional. If set to True, the dictionaries in the input list of faces are transfered to the faces of the resulting shell. The default is False. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created Shell. """ 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.Helper import Helper from topologicpy.Topology import Topology from topologicpy.Grid import Grid from topologicpy.Dictionary import Dictionary def removeShards(edges, hostTopology, maximumGap=0.5): returnEdges = [] for e in tqdm(edges, desc="Removing Shards", leave=False): if Edge.Length(e) < maximumGap: sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) sEdges = Topology.SuperTopologies(sv, hostTopology, "edge") sn = len(sEdges) eEdges = Topology.SuperTopologies(ev, hostTopology, "edge") en = len(eEdges) if sn >= 2 and en >= 2: returnEdges.append(e) else: returnEdges.append(e) return returnEdges def extendEdges(edges, hostTopology, maximumGap=0.5): returnEdges = [] for e in tqdm(edges, desc="Extending Edges", leave=False): sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) sEdges = Topology.SuperTopologies(sv, hostTopology, "edge") sn = len(sEdges) eEdges = Topology.SuperTopologies(ev, hostTopology, "edge") en = len(eEdges) if sn == 1: ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=True) returnEdges.append(ee) elif en == 1: ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=False) returnEdges.append(ee) else: returnEdges.append(e) return returnEdges facesCluster = Cluster.ByTopologies(faces) internalBoundary = Face.ByWire(Face.InternalBoundaries(externalBoundary)[0], tolerance=tolerance) bb = Topology.BoundingBox(internalBoundary) bb_d = Topology.Dictionary(bb) unitU = Dictionary.ValueAtKey(bb_d, 'width') / uSides unitV = Dictionary.ValueAtKey(bb_d, 'length') / vSides uRange = [u*unitU for u in range(uSides)] vRange = [v*unitV for v in range(vSides)] grid = Grid.EdgesByDistances(internalBoundary, uRange=uRange, vRange=vRange, clip=True) grid = Topology.Slice(internalBoundary, grid, tolerance=tolerance) grid_faces = Topology.Faces(grid) skeletons = [] for ib in tqdm(grid_faces, desc="Processing "+str(len(grid_faces))+" tiles", leave=False): building_shell = Topology.Slice(ib, facesCluster, tolerance=tolerance) wall_faces = Topology.Faces(building_shell) walls = [] for w1 in wall_faces: iv = Topology.InternalVertex(w1, tolerance=tolerance) flag = False for w2 in faces: if Vertex.IsInternal(iv, w2): flag = True break; if flag == False: walls.append(w1) for wall in walls: skeleton = Wire.Skeleton(wall, tolerance=0.001) # This tolerance works better. skeleton = Topology.Difference(skeleton, facesCluster, tolerance=tolerance) skeleton = Topology.Difference(skeleton, Face.Wire(wall), tolerance=tolerance) skeletons.append(skeleton) if len(skeletons) > 0: skeleton_cluster = Cluster.ByTopologies(skeletons+[internalBoundary]) skEdges = Topology.SelfMerge(Cluster.ByTopologies(removeShards(Topology.Edges(skeleton_cluster), skeleton_cluster, maximumGap=maximumGap)), tolerance=tolerance) if Topology.IsInstance(skEdges, "Edge"): skEdges = extendEdges([skEdges], skEdges, maximumGap=maximumGap) else: skEdges = extendEdges(Topology.Edges(skEdges), skEdges, maximumGap=maximumGap) if len(skEdges) < 1: print("ShellByDisjointFaces - Warning: No edges were extended.") #return Cluster.ByTopologies(skEdges) #print("ShellByDisjointFaces - Error: Could not derive central skeleton of interior walls. Returning None.") #return None shell = Topology.Slice(internalBoundary, skeleton_cluster, tolerance=tolerance) if mergeJunctions == True: vertices = Shell.Vertices(shell) centers = [] used = [] for v in vertices: for w in vertices: if not Topology.IsSame(v, w) and not w in used: if Vertex.Distance(v, w) < threshold: centers.append(v) used.append(w) edges = Shell.Edges(shell) new_edges = [] for e in edges: sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) for v in centers: if Vertex.Distance(sv, v) < threshold: sv = v if Vertex.Distance(ev, v) < threshold: ev = v new_edges.append(Edge.ByVertices([sv,ev], tolerance=tolerance)) cluster = Cluster.ByTopologies(new_edges) vertices = Topology.Vertices(cluster) edges = Topology.Edges(shell) xList = list(set([Vertex.X(v) for v in vertices])) xList.sort() xList = Helper.MergeByThreshold(xList, 0.5) yList = list(set([Vertex.Y(v) for v in vertices])) yList.sort() yList = Helper.MergeByThreshold(yList, 0.5) yList.sort() centers = [] new_edges = [] for e in edges: sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) svx = Vertex.X(sv) svy = Vertex.Y(sv) evx = Vertex.X(ev) evy = Vertex.Y(ev) for x in xList: if abs(svx-x) < threshold: svx = x break; for y in yList: if abs(svy-y) < threshold: svy = y break; sv = Vertex.ByCoordinates(svx, svy, 0) for x in xList: if abs(evx-x) < threshold: evx = x break; for y in yList: if abs(evy-y) < threshold: evy = y break; sv = Vertex.ByCoordinates(svx, svy, 0) ev = Vertex.ByCoordinates(evx, evy, 0) new_edges.append(Edge.ByVertices([sv, ev], tolerance=tolerance)) cluster = Cluster.ByTopologies(new_edges) eb = Face.ByWire(Shell.ExternalBoundary(shell), tolerance=tolerance) shell = Topology.Slice(eb, cluster, tolerance=tolerance) if not Topology.IsInstance(shell, "Shell"): try: temp_wires = [Wire.RemoveCollinearEdges(w, angTolerance=1.0) for w in Topology.Wires(shell)] temp_faces = [Face.ByWire(w, tolerance=tolerance) for w in temp_wires] except: temp_faces = Topology.Faces(shell) shell = Shell.ByFaces(temp_faces, tolerance=tolerance) if transferDictionaries == True: selectors = [] for f in faces: d = Topology.Dictionary(f) s = Topology.InternalVertex(f, tolerance=tolerance) s = Topology.SetDictionary(s, d) selectors.append(s) _ = Topology.TransferDictionariesBySelectors(topology=shell, selectors=selectors, tranFaces=True, tolerance=tolerance) return shell return None @staticmethod def ByFaces(faces: list, tolerance: float = 0.0001): """ Creates a shell from the input list of faces. Parameters ---------- faces : list The input list of faces. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created Shell. """ from topologicpy.Topology import Topology if not isinstance(faces, list): return None faceList = [x for x in faces if Topology.IsInstance(x, "Face")] if len(faceList) == 0: print("Shell.ByFaces - Error: The input faces list does not contain any valid faces. Returning None.") return None shell = topologic.Shell.ByFaces(faceList, tolerance) # Hook to Core if not Topology.IsInstance(shell, "Shell"): shell = Topology.SelfMerge(shell, tolerance=tolerance) if Topology.IsInstance(shell, "Shell"): return shell else: print("Shell.ByFaces - Error: Could not create shell. Returning None.") return None else: return shell @staticmethod def ByFacesCluster(cluster, tolerance: float = 0.0001): """ Creates a shell from the input cluster of faces. Parameters ---------- cluster : topologic_core.Cluster The input cluster of faces. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created shell. """ from topologicpy.Topology import Topology if not Topology.IsInstance(cluster, "Cluster"): return None faces = [] _ = cluster.Faces(None, faces) return Shell.ByFaces(faces, tolerance=tolerance) @staticmethod def ByWires(wires: list, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False): """ Creates a shell by lofting through the input wires Parameters ---------- wires : list The input list of wires. triangulate : bool , optional If set to True, the faces will be triangulated. The default is True. 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.Shell The creates shell. """ from topologicpy.Edge import Edge from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology if not isinstance(wires, list): return None wireList = [x for x in wires if Topology.IsInstance(x, "Wire")] faces = [] for i in range(len(wireList)-1): wire1 = wireList[i] wire2 = wireList[i+1] if Topology.Type(wire1) < Topology.TypeID("Edge") or Topology.Type(wire2) < Topology.TypeID("Edge"): return None if Topology.Type(wire1) == Topology.TypeID("Edge"): w1_edges = [wire1] else: w1_edges = Topology.Edges(wire1) if Topology.Type(wire2) == Topology.TypeID("Edge"): w2_edges = [wire2] else: w2_edges = Topology.Edges(wire2) if len(w1_edges) != len(w2_edges): return None if triangulate == True: for j in range (len(w1_edges)): e1 = w1_edges[j] e2 = w2_edges[j] e3 = None e4 = None try: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) except: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e4], tolerance=tolerance), tolerance=tolerance)) try: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) except: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e3],tolerance=tolerance), tolerance=tolerance)) if e3 and e4: e5 = Edge.ByVertices([e1.StartVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) faces.append(Face.ByWire(Wire.ByEdges([e1, e5, e4], tolerance=tolerance), tolerance=tolerance)) faces.append(Face.ByWire(Wire.ByEdges([e2, e5, e3], tolerance=tolerance), tolerance=tolerance)) else: for j in range (len(w1_edges)): e1 = w1_edges[j] e2 = w2_edges[j] e3 = None e4 = None try: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) except: try: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) except: pass try: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) except: try: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) except: pass if e3 and e4: try: faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2, e3], tolerance=tolerance), tolerance=tolerance)) except: faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2, e4], tolerance=tolerance), tolerance=tolerance)) elif e3: faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2], tolerance=tolerance), tolerance=tolerance)) elif e4: faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2], tolerance=tolerance), tolerance=tolerance)) return Shell.ByFaces(faces, tolerance=tolerance) @staticmethod def ByWiresCluster(cluster, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False): """ Creates a shell by lofting through the input cluster of wires Parameters ---------- wires : topologic_core.Cluster The input cluster of wires. triangulate : bool , optional If set to True, the faces will be triangulated. The default is True. 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.Shell The creates shell. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not cluster: return None if not Topology.IsInstance(cluster, "Cluster"): return None wires = Cluster.Wires(cluster) return Shell.ByWires(wires, triangulate=triangulate, tolerance=tolerance, silent=silent) @staticmethod def Circle(origin= None, radius: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, 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 32. 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. 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 pie. 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.Shell The created circle. """ return Shell.Pie(origin=origin, radiusA=radius, radiusB=0, sides=sides, rings=1, fromAngle=fromAngle, toAngle=toAngle, direction=direction, placement=placement, tolerance=tolerance) @staticmethod def Delaunay(vertices: list, face= None, tolerance: float = 0.0001): """ Returns a delaunay partitioning of the input vertices. The vertices must be coplanar. See https://en.wikipedia.org/wiki/Delaunay_triangulation. Parameters ---------- vertices : list The input list of vertices. face : topologic_core.Face , optional The input face. If specified, the delaunay triangulation is clipped to the face. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- shell A shell representing the delaunay triangulation of the input vertices. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import sample from scipy.spatial import Delaunay as SCIDelaunay import numpy as np if not isinstance(vertices, list): return None vertices = [x for x in vertices if Topology.IsInstance(x, "Vertex")] if len(vertices) < 3: return None # Create a Vertex at the world's origin (0, 0, 0) world_origin = Vertex.Origin() if Topology.IsInstance(face, "Face"): # Flatten the face origin = Topology.Centroid(face) normal = Face.Normal(face) flatFace = Topology.Flatten(face, origin=origin, direction=normal) faceVertices = Face.Vertices(face) vertices += faceVertices # Create a cluster of the input vertices verticesCluster = Cluster.ByTopologies(vertices) # Flatten the cluster using the same transformations verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal) vertices = Cluster.Vertices(verticesCluster) points = [] for v in vertices: points.append([Vertex.X(v), Vertex.Y(v)]) delaunay = SCIDelaunay(points) simplices = delaunay.simplices faces = [] for simplex in simplices: tempTriangleVertices = [] tempTriangleVertices.append(vertices[simplex[0]]) tempTriangleVertices.append(vertices[simplex[1]]) tempTriangleVertices.append(vertices[simplex[2]]) tempFace = Face.ByWire(Wire.ByVertices(tempTriangleVertices), tolerance=tolerance) faces.append(tempFace) shell = Shell.ByFaces(faces, tolerance=tolerance) if shell == None: shell = Cluster.ByTopologies(faces) if Topology.IsInstance(face, "Face"): edges = Topology.Edges(shell) shell = Topology.Slice(flatFace, Cluster.ByTopologies(edges)) # Get the internal boundaries of the face wires = Face.InternalBoundaries(flatFace) ibList = [] if len(wires) > 0: ibList = [Face.ByWire(w) for w in wires] cluster = Cluster.ByTopologies(ibList) shell = Topology.Difference(shell, cluster) shell = Topology.Unflatten(shell, origin=origin, direction=normal) return shell @staticmethod def Edges(shell) -> list: """ Returns the edges of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of edges. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None edges = [] _ = shell.Edges(None, edges) return edges @staticmethod def ExternalBoundary(shell, tolerance: float = 0.0001): """ Returns the external boundary of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire or topologic_core.Cluster The external boundary of the input shell. If the shell has holes, the return value will be a cluster of wires. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None edges = [] _ = shell.Edges(None, edges) obEdges = [] for anEdge in edges: faces = [] _ = anEdge.Faces(shell, faces) if len(faces) == 1: obEdges.append(anEdge) return Topology.SelfMerge(Cluster.ByTopologies(obEdges), tolerance=tolerance) @staticmethod def Faces(shell) -> list: """ Returns the faces of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of faces. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None faces = [] _ = shell.Faces(None, faces) return faces @staticmethod def IsOnBoundary(shell, vertex, tolerance: float = 0.0001) -> bool: """ Returns True if the input vertex is on the boundary of the input shell. Returns False otherwise. On the boundary is defined as being on the boundary of one of the shell's external or internal boundaries Parameters ---------- shell : topologic_core.Shell The input shell. vertex : topologic_core.Vertex The input vertex. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- bool Returns True if the input vertex is inside the input shell. Returns False otherwise. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None if not Topology.IsInstance(vertex, "Vertex"): return None boundary = Shell.ExternalBoundary(shell, tolerance=tolerance) if Vertex.IsInternal(vertex, boundary, tolerance=tolerance): return True internal_boundaries = Shell.InternalBoundaries(shell, tolerance=tolerance) for ib in internal_boundaries: if Vertex.IsInternal(vertex, ib, tolerance=tolerance): return True return False @staticmethod def HyperbolicParaboloidRectangularDomain(origin= None, llVertex= None, lrVertex= None, ulVertex= None, urVertex= None, uSides: int = 10, vSides: int = 10, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a hyperbolic paraboloid with a rectangular domain. Parameters ---------- origin : topologic_core.Vertex , optional The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None. llVertex : topologic_core.Vertex , optional The lower left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, -0.5, -0.5). lrVertex : topologic_core.Vertex , optional The lower right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, -0.5, 0.5). ulVertex : topologic_core.Vertex , optional The upper left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, 0.5, 0.5). urVertex : topologic_core.Vertex , optional The upper right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, 0.5, -0.5). uSides : int , optional The number of segments along the X axis. The default is 10. vSides : int , optional The number of segments along the Y axis. The default is 10. direction : list , optional The vector representing the up direction of the hyperbolic parabolid. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the hyperbolic parabolid. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created hyperbolic paraboloid. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Topology import Topology if not Topology.IsInstance(origin, "Vertex"): origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(llVertex, "Vertex"): llVertex = Vertex.ByCoordinates(-0.5, -0.5, -0.5) if not Topology.IsInstance(lrVertex, "Vertex"): lrVertex = Vertex.ByCoordinates(0.5, -0.5, 0.5) if not Topology.IsInstance(ulVertex, "Vertex"): ulVertex = Vertex.ByCoordinates(-0.5, 0.5, 0.5) if not Topology.IsInstance(urVertex, "Vertex"): urVertex = Vertex.ByCoordinates(0.5, 0.5, -0.5) e1 = Edge.ByVertices([llVertex, lrVertex], tolerance=tolerance) e3 = Edge.ByVertices([urVertex, ulVertex], tolerance=tolerance) edges = [] for i in range(uSides+1): v1 = Edge.VertexByParameter(e1, float(i)/float(uSides)) v2 = Edge.VertexByParameter(e3, 1.0 - float(i)/float(uSides)) edges.append(Edge.ByVertices([v1, v2], tolerance=tolerance)) faces = [] for i in range(uSides): for j in range(vSides): v1 = Edge.VertexByParameter(edges[i], float(j)/float(vSides)) v2 = Edge.VertexByParameter(edges[i], float(j+1)/float(vSides)) v3 = Edge.VertexByParameter(edges[i+1], float(j+1)/float(vSides)) v4 = Edge.VertexByParameter(edges[i+1], float(j)/float(vSides)) faces.append(Face.ByVertices([v1, v2, v4])) faces.append(Face.ByVertices([v4, v2, v3])) returnTopology = Shell.ByFaces(faces, tolerance=tolerance) if not returnTopology: returnTopology = None xOffset = 0 yOffset = 0 zOffset = 0 minX = min([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()]) maxX = max([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()]) minY = min([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()]) maxY = max([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()]) minZ = min([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()]) maxZ = max([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()]) if placement.lower() == "lowerleft": xOffset = -minX yOffset = -minY zOffset = -minZ elif placement.lower() == "bottom": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -minZ elif placement.lower() == "center": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -(minZ + (maxZ - minZ)*0.5) returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset) returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin) returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction) return returnTopology @staticmethod def HyperbolicParaboloidCircularDomain(origin= None, radius: float = 0.5, sides: int = 36, rings: int = 10, A: float = 2.0, B: float = -2.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a hyperbolic paraboloid with a circular domain. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape Parameters ---------- origin : topologic_core.Vertex , optional The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None. radius : float , optional The desired radius of the hyperbolic paraboloid. The default is 0.5. sides : int , optional The desired number of sides of the hyperbolic parabolid. The default is 36. rings : int , optional The desired number of concentric rings of the hyperbolic parabolid. The default is 10. A : float , optional The *A* constant in the equation z = A*x^2^ + B*y^2^. The default is 2.0. B : float , optional The *B* constant in the equation z = A*x^2^ + B*y^2^. The default is -2.0. direction : list , optional The vector representing the up direction of the hyperbolic paraboloid. 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", "bottom". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created hyperboloic paraboloid. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(origin, "Vertex"): origin = Vertex.ByCoordinates(0, 0, 0) uOffset = float(360)/float(sides) vOffset = float(radius)/float(rings) faces = [] for i in range(rings-1): r1 = radius - vOffset*i r2 = radius - vOffset*(i+1) for j in range(sides-1): a1 = math.radians(uOffset)*j a2 = math.radians(uOffset)*(j+1) x1 = math.sin(a1)*r1 y1 = math.cos(a1)*r1 z1 = A*x1*x1 + B*y1*y1 x2 = math.sin(a1)*r2 y2 = math.cos(a1)*r2 z2 = A*x2*x2 + B*y2*y2 x3 = math.sin(a2)*r2 y3 = math.cos(a2)*r2 z3 = A*x3*x3 + B*y3*y3 x4 = math.sin(a2)*r1 y4 = math.cos(a2)*r1 z4 = A*x4*x4 + B*y4*y4 v1 = Vertex.ByCoordinates(x1,y1,z1) v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) v4 = Vertex.ByCoordinates(x4,y4,z4) f1 = Face.ByVertices([v1,v2,v4]) f2 = Face.ByVertices([v4,v2,v3]) faces.append(f1) faces.append(f2) a1 = math.radians(uOffset)*(sides-1) a2 = math.radians(360) x1 = math.sin(a1)*r1 y1 = math.cos(a1)*r1 z1 = A*x1*x1 + B*y1*y1 x2 = math.sin(a1)*r2 y2 = math.cos(a1)*r2 z2 = A*x2*x2 + B*y2*y2 x3 = math.sin(a2)*r2 y3 = math.cos(a2)*r2 z3 = A*x3*x3 + B*y3*y3 x4 = math.sin(a2)*r1 y4 = math.cos(a2)*r1 z4 = A*x4*x4 + B*y4*y4 v1 = Vertex.ByCoordinates(x1,y1,z1) v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) v4 = Vertex.ByCoordinates(x4,y4,z4) f1 = Face.ByVertices([v1,v2,v4]) f2 = Face.ByVertices([v4,v2,v3]) faces.append(f1) faces.append(f2) # Special Case: Center triangles r = vOffset x1 = 0 y1 = 0 z1 = 0 v1 = Vertex.ByCoordinates(x1,y1,z1) for j in range(sides-1): a1 = math.radians(uOffset)*j a2 = math.radians(uOffset)*(j+1) x2 = math.sin(a1)*r y2 = math.cos(a1)*r z2 = A*x2*x2 + B*y2*y2 #z2 = 0 x3 = math.sin(a2)*r y3 = math.cos(a2)*r z3 = A*x3*x3 + B*y3*y3 #z3 = 0 v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) f1 = Face.ByVertices([v2,v1,v3]) faces.append(f1) a1 = math.radians(uOffset)*(sides-1) a2 = math.radians(360) x2 = math.sin(a1)*r y2 = math.cos(a1)*r z2 = A*x2*x2 + B*y2*y2 x3 = math.sin(a2)*r y3 = math.cos(a2)*r z3 = A*x3*x3 + B*y3*y3 v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) f1 = Face.ByVertices([v2,v1,v3]) faces.append(f1) returnTopology = Shell.ByFaces(faces, tolerance) if not returnTopology: returnTopology = Cluster.ByTopologies(faces) vertices = [] _ = returnTopology.Vertices(None, vertices) xList = [] yList = [] zList = [] for aVertex in vertices: xList.append(aVertex.X()) yList.append(aVertex.Y()) zList.append(aVertex.Z()) minX = min(xList) maxX = max(xList) minY = min(yList) maxY = max(yList) minZ = min(zList) maxZ = max(zList) xOffset = 0 yOffset = 0 zOffset = 0 if placement.lower() == "lowerleft": xOffset = -minX yOffset = -minY zOffset = -minZ elif placement.lower() == "bottom": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -minZ elif placement.lower() == "center": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -(minZ + (maxZ - minZ)*0.5) returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset) returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin) returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction) return returnTopology @staticmethod def InternalBoundaries(shell, tolerance=0.0001): """ Returns the internal boundaries (closed wires) of the input shell. Internal boundaries are considered holes. Parameters ---------- shell : topologic_core.Shell The input shell. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- list The list of internal boundaries """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology edges = [] _ = shell.Edges(None, edges) ibEdges = [] for anEdge in edges: faces = [] _ = anEdge.Faces(shell, faces) if len(faces) > 1: ibEdges.append(anEdge) returnTopology = Topology.SelfMerge(Cluster.ByTopologies(ibEdges), tolerance=tolerance) wires = Topology.Wires(returnTopology) return wires @staticmethod def IsClosed(shell) -> bool: """ Returns True if the input shell is closed. Returns False otherwise. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- bool True if the input shell is closed. False otherwise. """ return shell.IsClosed() @staticmethod def Pie(origin= None, radiusA: float = 0.5, radiusB: float = 0.0, sides: int = 32, rings: int = 1, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a pie shape. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the pie. The default is None which results in the pie being placed at (0, 0, 0). radiusA : float , optional The outer radius of the pie. The default is 0.5. radiusB : float , optional The inner radius of the pie. The default is 0.25. sides : int , optional The number of sides of the pie. The default is 32. rings : int , optional The number of rings of the pie. The default is 1. fromAngle : float , optional The angle in degrees from which to start creating the arc of the pie. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the pie. The default is 360. direction : list , optional The vector representing the up direction of the pie. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the pie. 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.Shell The created pie. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None if toAngle < fromAngle: toAngle += 360 if abs(toAngle-fromAngle) < tolerance: return None fromAngle = math.radians(fromAngle) toAngle = math.radians(toAngle) angleRange = toAngle - fromAngle radiusA = abs(radiusA) radiusB = abs(radiusB) if radiusB > radiusA: temp = radiusA radiusA = radiusB radiusB = temp if abs(radiusA - radiusB) < tolerance or radiusA < tolerance: return None radiusRange = radiusA - radiusB sides = int(abs(math.floor(sides))) if sides < 3: return None rings = int(abs(rings)) if radiusB < tolerance: radiusB = 0 xOffset = 0 yOffset = 0 zOffset = 0 if placement.lower() == "lowerleft": xOffset = radiusA yOffset = radiusA uOffset = float(angleRange)/float(sides) vOffset = float(radiusRange)/float(rings) faces = [] if radiusB > tolerance: for i in range(rings): r1 = radiusA - vOffset*i r2 = radiusA - vOffset*(i+1) for j in range(sides): a1 = fromAngle + uOffset*j a2 = fromAngle + uOffset*(j+1) x1 = math.sin(a1)*r1 y1 = math.cos(a1)*r1 z1 = 0 x2 = math.sin(a1)*r2 y2 = math.cos(a1)*r2 z2 = 0 x3 = math.sin(a2)*r2 y3 = math.cos(a2)*r2 z3 = 0 x4 = math.sin(a2)*r1 y4 = math.cos(a2)*r1 z4 = 0 v1 = Vertex.ByCoordinates(x1,y1,z1) v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) v4 = Vertex.ByCoordinates(x4,y4,z4) f1 = Face.ByVertices([v1,v2,v3,v4]) faces.append(f1) else: x1 = 0 y1 = 0 z1 = 0 v1 = Vertex.ByCoordinates(x1,y1,z1) for j in range(sides): a1 = fromAngle + uOffset*j a2 = fromAngle + uOffset*(j+1) x2 = math.sin(a1)*radiusA y2 = math.cos(a1)*radiusA z2 = 0 x3 = math.sin(a2)*radiusA y3 = math.cos(a2)*radiusA z3 = 0 v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) f1 = Face.ByVertices([v2,v1,v3]) faces.append(f1) shell = Shell.ByFaces(faces, tolerance=tolerance) if not shell: return None shell = Topology.Translate(shell, xOffset, yOffset, zOffset) shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin) shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction) return shell @staticmethod def Planarize(shell, origin= None, mantissa: int = 6, tolerance: float = 0.0001): """ Returns a planarized version of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. 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 shell will be projected. If set to None, the centroid of the input shell will be chosen. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- topologic_core.Shell The planarized shell. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): print("Shell.Planarize - Error: The input wire parameter is not a valid topologic shell. Returning None.") return None if origin == None: origin = Vertex.Origin() if not Topology.IsInstance(origin, "Vertex"): print("Shell.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None vertices = Topology.Vertices(shell) faces = Topology.Faces(shell) 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_shell = Topology.ReplaceVertices(shell, verticesA=vertices, verticesB=new_vertices) new_faces = Topology.Faces(new_shell) return Topology.SelfMerge(Cluster.ByTopologies(new_faces), tolerance=tolerance) @staticmethod def Rectangle(origin= None, width: float = 1.0, length: float = 1.0, uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1], placement: str = "center", 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. uSides : int , optional The number of sides along the width. The default is 2. vSides : int , optional The number of sides along the length. The default is 2. 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", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created shell. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None uOffset = float(width)/float(uSides) vOffset = float(length)/float(vSides) faces = [] if placement.lower() == "center": wOffset = width*0.5 lOffset = length*0.5 else: wOffset = 0 lOffset = 0 for i in range(uSides): for j in range(vSides): rOrigin = Vertex.ByCoordinates(i*uOffset - wOffset, j*vOffset - lOffset, 0) w = Wire.Rectangle(origin=rOrigin, width=uOffset, length=vOffset, direction=[0, 0, 1], placement="lowerleft", tolerance=tolerance) f = Face.ByWire(w, tolerance=tolerance) faces.append(f) shell = Shell.ByFaces(faces, tolerance=tolerance) shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin) shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction) return shell @staticmethod def RemoveCollinearEdges(shell, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Removes any collinear edges in the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. 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.Shell The created shell without any collinear edges. """ from topologicpy.Face import Face from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): print("Shell.RemoveCollinearEdges - Error: The input shell parameter is not a valid shell. Returning None.") return None faces = Shell.Faces(shell) clean_faces = [] for face in faces: clean_faces.append(Face.RemoveCollinearEdges(face, angTolerance=angTolerance, tolerance=tolerance)) return Shell.ByFaces(clean_faces, tolerance=tolerance) @staticmethod def Roof(face, angle: float = 45, epsilon: float = 0.01, 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. epsilon : float , optional The desired epsilon (another form of tolerance for distance from plane). The default is 0.01. (This is set to a larger number as it was found to work better) 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.Shell The created roof. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Shell import Shell from topologicpy.Cell import Cell from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary import topologic_core as topologic import math def nearest_vertex_2d(v, vertices, tolerance=0.001): for vertex in vertices: x2 = Vertex.X(vertex) y2 = Vertex.Y(vertex) temp_v = Vertex.ByCoordinates(x2, y2, Vertex.Z(v)) if Vertex.Distance(v, temp_v) <= tolerance: return vertex return None if not Topology.IsInstance(face, "Face"): return None angle = abs(angle) if angle >= 90-tolerance: return None if angle < tolerance: return None origin = Topology.Centroid(face) normal = Face.Normal(face) flat_face = Topology.Flatten(face, origin=origin, direction=normal) roof = Wire.Roof(flat_face, angle=angle, tolerance=tolerance) if not roof: return None shell = Shell.Skeleton(flat_face, tolerance=tolerance) faces = Shell.Faces(shell) Topology.Show(shell) if not faces: return None triangles = [] for face in faces: internalBoundaries = Face.InternalBoundaries(face) if len(internalBoundaries) == 0: if len(Topology.Vertices(face)) > 3: triangles += Face.Triangulate(face, tolerance=tolerance) else: triangles += [face] roof_vertices = Topology.Vertices(roof) flat_vertices = [] for rv in roof_vertices: flat_vertices.append(Vertex.ByCoordinates(Vertex.X(rv), Vertex.Y(rv), 0)) final_triangles = [] for triangle in triangles: if len(Topology.Vertices(triangle)) > 3: triangles = Face.Triangulate(triangle, tolerance=tolerance) else: triangles = [triangle] final_triangles += triangles final_faces = [] for triangle in final_triangles: face_vertices = Topology.Vertices(triangle) top_vertices = [] for sv in face_vertices: temp = nearest_vertex_2d(sv, roof_vertices, tolerance=tolerance) if temp: top_vertices.append(temp) else: top_vertices.append(sv) tri_face = Face.ByVertices(top_vertices) final_faces.append(tri_face) shell = Shell.ByFaces(final_faces, tolerance=tolerance) if not shell: shell = Cluster.ByTopologies(final_faces) try: shell = Topology.RemoveCoplanarFaces(shell, epsilon=epsilon, tolerance=tolerance) except: pass return shell @staticmethod def SelfMerge(shell, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates a face by merging the faces of the input shell. The shell must be planar within the input angular tolerance. Parameters ---------- shell : topologic_core.Shell The input shell. 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.Face The created face. """ from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Shell import Shell from topologicpy.Topology import Topology def planarizeList(wireList): returnList = [] for aWire in wireList: returnList.append(Wire.Planarize(aWire)) return returnList if not Topology.IsInstance(shell, "Shell"): return None ext_boundary = Shell.ExternalBoundary(shell, tolerance=tolerance) if Topology.IsInstance(ext_boundary, "Wire"): f = Face.ByWire(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance) or Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance)) if not f: print("FaceByPlanarShell - Error: The input Wire is not planar and could not be fixed. Returning None.") return None else: return f elif Topology.IsInstance(ext_boundary, "Cluster"): wires = [] _ = ext_boundary.Wires(None, wires) faces = [] areas = [] for aWire in wires: try: aFace = Face.ByWire(Topology.RemoveCollinearEdges(aWire, angTolerance)) except: aFace = Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(aWire, angTolerance))) anArea = Face.Area(aFace) faces.append(aFace) areas.append(anArea) max_index = areas.index(max(areas)) ext_boundary = faces[max_index] int_boundaries = list(set(faces) - set([ext_boundary])) int_wires = [] for int_boundary in int_boundaries: temp_wires = [] _ = int_boundary.Wires(None, temp_wires) int_wires.append(Topology.RemoveCollinearEdges(temp_wires[0], angTolerance)) temp_wires = [] _ = ext_boundary.Wires(None, temp_wires) ext_wire = Topology.RemoveCollinearEdges(temp_wires[0], angTolerance) try: return Face.ByWires(ext_wire, int_wires, tolerance=tolerance) except: return Face.ByWires(Wire.Planarize(ext_wire), planarizeList(int_wires), tolerance=tolerance) else: return None def Skeleton(face, tolerance: float = 0.001): """ Creates a shell 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. 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.Shell The created straight skeleton. """ from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology import topologic_core as topologic import math if not Topology.IsInstance(face, "Face"): return None roof = Wire.Skeleton(face, tolerance=tolerance) if not roof: return None br = Wire.BoundingRectangle(roof) #This works even if it is a Cluster not a Wire br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1) bf = Face.ByWire(br, tolerance=tolerance) large_shell = Topology.Boolean(bf, roof, operation="slice", tolerance=tolerance) if not large_shell: return None faces = Topology.Faces(large_shell) if not faces: return None final_faces = [] for f in faces: internalBoundaries = Face.InternalBoundaries(f) if len(internalBoundaries) == 0: final_faces.append(f) shell = Shell.ByFaces(final_faces, tolerance=tolerance) return shell @staticmethod def Simplify(shell, simplifyBoundary=True, tolerance=0.0001): """ Simplifies the input shell 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 ---------- shell : topologic_core.Shell The input shell. simplifyBoundary : bool , optional If set to True, the external boundary of the shell will be simplified as well. Otherwise, it will not be simplified. The default is True. tolerance : float , optional The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed. Returns ------- topologic_core.Shell The simplified shell. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Shell import Shell from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Helper import Helper 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(shell, "Shell"): print("Shell.Simplify - Error: The input shell parameter is not a valid topologic shell. Returning None.") return None # Get the external boundary of the shell. This can be simplified as well, but might cause issues at the end. # At this point, it is assumed to be left as is. all_edges = Topology.Edges(shell) if simplifyBoundary == False: ext_boundary = Face.ByWire(Shell.ExternalBoundary(shell, tolerance=tolerance), tolerance=tolerance) # Get the internal edges of the shell. i_edges = [] for edge in all_edges: faces = Topology.SuperTopologies(edge, shell, topologyType="face") if len(faces) > 1: # This means that the edge separates two faces so it is internal. i_edges.append(edge) # Creat a Wire from the internal edges wire = Topology.SelfMerge(Cluster.ByTopologies(i_edges), tolerance=tolerance) else: wire = Topology.SelfMerge(Cluster.ByTopologies(all_edges), tolerance=tolerance) # Split the wires at its junctions (where more than two edges meet at a vertex) components = Wire.Split(wire) separators = [] wires = [] for component in components: if Topology.IsInstance(component, "Cluster"): component = Topology.SelfMerge(component, tolerance=tolerance) if Topology.IsInstance(component, "Cluster"): separators.append(Cluster.FreeEdges(component, tolerance=tolerance)) wires.append(Cluster.FreeWires(component, tolerance=tolerance)) if Topology.IsInstance(component, "Edge"): separators.append(component) if Topology.IsInstance(component, "Wire"): wires.append(component) if Topology.IsInstance(component, "Edge"): separators.append(component) if Topology.IsInstance(component, "Wire"): wires.append(component) wires = Helper.Flatten(wires) separators = Helper.Flatten(separators) results = [] for w in wires: temp_wire = Wire.ByVertices(douglas_peucker(w, tolerance), close=False) results.append(temp_wire) # Make a Cluster out of the results cluster = Cluster.ByTopologies(results) # Get all the edges of the result edges = Topology.Edges(cluster) # Add them to the final edges final_edges = edges + separators # Make a Cluster out of the final set of edges cluster = Cluster.ByTopologies(final_edges) if simplifyBoundary == False: # Slice the external boundary of the shell by the cluster final_result = Topology.Slice(ext_boundary, cluster, tolerance=tolerance) else: br = Wire.BoundingRectangle(shell) br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1.5) br = Face.ByWire(br, tolerance=tolerance) v = Face.VertexByParameters(br, 0.1, 0.1) result = Topology.Slice(br, cluster, tolerance=tolerance) faces = Topology.Faces(result) final_faces = [] for face in faces: if not Vertex.IsInternal(v, face, tolerance=0.01): final_faces.append(face) final_result = Shell.ByFaces(final_faces, tolerance=tolerance) return final_result @staticmethod def Vertices(shell) -> list: """ Returns the vertices of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of vertices. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None vertices = [] _ = shell.Vertices(None, vertices) return vertices @staticmethod def Voronoi(vertices: list, face= None, tolerance: float = 0.0001): """ Returns a voronoi partitioning of the input face based on the input vertices. The vertices must be coplanar and within the face. See https://en.wikipedia.org/wiki/Voronoi_diagram. Parameters ---------- vertices : list The input list of vertices. face : topologic_core.Face , optional The input face. If the face is not set an optimised bounding rectangle of the input vertices is used instead. The default is None. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- shell A shell representing the voronoi partitioning of the input face. """ 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.Dictionary import Dictionary if not Topology.IsInstance(face, "Face"): cluster = Cluster.ByTopologies(vertices) br = Wire.BoundingRectangle(cluster, optimize=5) face = Face.ByWire(br, tolerance=tolerance) if not isinstance(vertices, list): return None vertices = [x for x in vertices if Topology.IsInstance(x, "Vertex")] if len(vertices) < 2: return None # Flatten the input face origin = Topology.Centroid(face) normal = Face.Normal(face) flatFace = Topology.Flatten(face, origin=origin, direction=normal) eb = Face.ExternalBoundary(flatFace) ibList = Face.InternalBoundaries(flatFace) temp_verts = Topology.Vertices(eb) new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts] eb = Wire.ByVertices(new_verts, close=True) new_ibList = [] for ib in ibList: temp_verts = Topology.Vertices(ib) new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts] new_ibList.append(Wire.ByVertices(new_verts, close=True)) flatFace = Face.ByWires(eb, new_ibList) # Create a cluster of the input vertices verticesCluster = Cluster.ByTopologies(vertices) # Flatten the cluster using the same transformations verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal) flatVertices = Topology.Vertices(verticesCluster) flatVertices = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in flatVertices] points = [] for flatVertex in flatVertices: points.append([flatVertex.X(), flatVertex.Y()]) br = Wire.BoundingRectangle(flatFace) br_vertices = Wire.Vertices(br) br_x = [] br_y = [] for br_v in br_vertices: x, y = Vertex.Coordinates(br_v, outputType="xy") br_x.append(x) br_y.append(y) min_x = min(br_x) max_x = max(br_x) min_y = min(br_y) max_y = max(br_y) br_width = abs(max_x - min_x) br_length = abs(max_y - min_y) points.append((-br_width*4, -br_length*4)) points.append((-br_width*4, br_length*4)) points.append((br_width*4, -br_length*4)) points.append((br_width*4, br_length*4)) voronoi = Voronoi(points, furthest_site=False) voronoiVertices = [] for v in voronoi.vertices: voronoiVertices.append(Vertex.ByCoordinates(v[0], v[1], 0)) faces = [] for region in voronoi.regions: tempWire = [] if len(region) > 1 and not -1 in region: for v in region: tempWire.append(Vertex.ByCoordinates(voronoiVertices[v].X(), voronoiVertices[v].Y(), 0)) temp_verts = [] for v in tempWire: if len(temp_verts) == 0: temp_verts.append(v) elif Vertex.Index(v, temp_verts) == None: temp_verts.append(v) tempWire = temp_verts temp_w = Wire.ByVertices(tempWire, close=True) faces.append(Face.ByWire(Wire.ByVertices(tempWire, close=True), tolerance=tolerance)) shell = Shell.ByFaces(faces, tolerance=tolerance) edges = Shell.Edges(shell) edgesCluster = Cluster.ByTopologies(edges) shell = Topology.Slice(flatFace,edgesCluster, tolerance=tolerance) shell = Topology.Unflatten(shell, origin=origin, direction=normal) return shell @staticmethod def Wires(shell) -> list: """ Returns the wires of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of wires. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None wires = [] _ = shell.Wires(None, wires) return wires
Static methods
def ByDisjointFaces(externalBoundary, faces, maximumGap=0.5, mergeJunctions=False, threshold=0.5, uSides=1, vSides=1, transferDictionaries=False, tolerance=0.0001)
-
Creates a shell from an input list of disjointed faces. THIS IS STILL EXPERIMENTAL
Parameters
externalBoundary
:topologic_core.Face
- The input external boundary of the faces. This resembles a ribbon (face with hole) where its interior boundary touches the edges of the input list of faces.
faces
:list
- The input list of faces.
maximumGap
:float
, optional- The length of the maximum gap between the faces. The default is 0.5.
mergeJunctions
:bool
, optional- If set to True, the interior junctions are merged into a single vertex. Otherwise, diagonal edges are added to resolve transitions between different gap distances.
threshold
:float
, optional- The desired threshold under which vertices are merged into a single vertex. The default is 0.5.
uSides
:int
, optional- The desired number of sides along the X axis for the grid that subdivides the input faces to aid in processing. The default is 1.
vSides
:int
, optional- The desired number of sides along the Y axis for the grid that subdivides the input faces to aid in processing. The default is 1.
- transferDictionaries : bool, optional.
- If set to True, the dictionaries in the input list of faces are transfered to the faces of the resulting shell. The default is False.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Shell
- The created Shell.
Expand source code
@staticmethod def ByDisjointFaces(externalBoundary, faces, maximumGap=0.5, mergeJunctions=False, threshold=0.5, uSides=1, vSides=1, transferDictionaries=False, tolerance=0.0001): """ Creates a shell from an input list of disjointed faces. THIS IS STILL EXPERIMENTAL Parameters ---------- externalBoundary : topologic_core.Face The input external boundary of the faces. This resembles a ribbon (face with hole) where its interior boundary touches the edges of the input list of faces. faces : list The input list of faces. maximumGap : float , optional The length of the maximum gap between the faces. The default is 0.5. mergeJunctions : bool , optional If set to True, the interior junctions are merged into a single vertex. Otherwise, diagonal edges are added to resolve transitions between different gap distances. threshold : float , optional The desired threshold under which vertices are merged into a single vertex. The default is 0.5. uSides : int , optional The desired number of sides along the X axis for the grid that subdivides the input faces to aid in processing. The default is 1. vSides : int , optional The desired number of sides along the Y axis for the grid that subdivides the input faces to aid in processing. The default is 1. transferDictionaries : bool, optional. If set to True, the dictionaries in the input list of faces are transfered to the faces of the resulting shell. The default is False. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created Shell. """ 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.Helper import Helper from topologicpy.Topology import Topology from topologicpy.Grid import Grid from topologicpy.Dictionary import Dictionary def removeShards(edges, hostTopology, maximumGap=0.5): returnEdges = [] for e in tqdm(edges, desc="Removing Shards", leave=False): if Edge.Length(e) < maximumGap: sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) sEdges = Topology.SuperTopologies(sv, hostTopology, "edge") sn = len(sEdges) eEdges = Topology.SuperTopologies(ev, hostTopology, "edge") en = len(eEdges) if sn >= 2 and en >= 2: returnEdges.append(e) else: returnEdges.append(e) return returnEdges def extendEdges(edges, hostTopology, maximumGap=0.5): returnEdges = [] for e in tqdm(edges, desc="Extending Edges", leave=False): sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) sEdges = Topology.SuperTopologies(sv, hostTopology, "edge") sn = len(sEdges) eEdges = Topology.SuperTopologies(ev, hostTopology, "edge") en = len(eEdges) if sn == 1: ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=True) returnEdges.append(ee) elif en == 1: ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=False) returnEdges.append(ee) else: returnEdges.append(e) return returnEdges facesCluster = Cluster.ByTopologies(faces) internalBoundary = Face.ByWire(Face.InternalBoundaries(externalBoundary)[0], tolerance=tolerance) bb = Topology.BoundingBox(internalBoundary) bb_d = Topology.Dictionary(bb) unitU = Dictionary.ValueAtKey(bb_d, 'width') / uSides unitV = Dictionary.ValueAtKey(bb_d, 'length') / vSides uRange = [u*unitU for u in range(uSides)] vRange = [v*unitV for v in range(vSides)] grid = Grid.EdgesByDistances(internalBoundary, uRange=uRange, vRange=vRange, clip=True) grid = Topology.Slice(internalBoundary, grid, tolerance=tolerance) grid_faces = Topology.Faces(grid) skeletons = [] for ib in tqdm(grid_faces, desc="Processing "+str(len(grid_faces))+" tiles", leave=False): building_shell = Topology.Slice(ib, facesCluster, tolerance=tolerance) wall_faces = Topology.Faces(building_shell) walls = [] for w1 in wall_faces: iv = Topology.InternalVertex(w1, tolerance=tolerance) flag = False for w2 in faces: if Vertex.IsInternal(iv, w2): flag = True break; if flag == False: walls.append(w1) for wall in walls: skeleton = Wire.Skeleton(wall, tolerance=0.001) # This tolerance works better. skeleton = Topology.Difference(skeleton, facesCluster, tolerance=tolerance) skeleton = Topology.Difference(skeleton, Face.Wire(wall), tolerance=tolerance) skeletons.append(skeleton) if len(skeletons) > 0: skeleton_cluster = Cluster.ByTopologies(skeletons+[internalBoundary]) skEdges = Topology.SelfMerge(Cluster.ByTopologies(removeShards(Topology.Edges(skeleton_cluster), skeleton_cluster, maximumGap=maximumGap)), tolerance=tolerance) if Topology.IsInstance(skEdges, "Edge"): skEdges = extendEdges([skEdges], skEdges, maximumGap=maximumGap) else: skEdges = extendEdges(Topology.Edges(skEdges), skEdges, maximumGap=maximumGap) if len(skEdges) < 1: print("ShellByDisjointFaces - Warning: No edges were extended.") #return Cluster.ByTopologies(skEdges) #print("ShellByDisjointFaces - Error: Could not derive central skeleton of interior walls. Returning None.") #return None shell = Topology.Slice(internalBoundary, skeleton_cluster, tolerance=tolerance) if mergeJunctions == True: vertices = Shell.Vertices(shell) centers = [] used = [] for v in vertices: for w in vertices: if not Topology.IsSame(v, w) and not w in used: if Vertex.Distance(v, w) < threshold: centers.append(v) used.append(w) edges = Shell.Edges(shell) new_edges = [] for e in edges: sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) for v in centers: if Vertex.Distance(sv, v) < threshold: sv = v if Vertex.Distance(ev, v) < threshold: ev = v new_edges.append(Edge.ByVertices([sv,ev], tolerance=tolerance)) cluster = Cluster.ByTopologies(new_edges) vertices = Topology.Vertices(cluster) edges = Topology.Edges(shell) xList = list(set([Vertex.X(v) for v in vertices])) xList.sort() xList = Helper.MergeByThreshold(xList, 0.5) yList = list(set([Vertex.Y(v) for v in vertices])) yList.sort() yList = Helper.MergeByThreshold(yList, 0.5) yList.sort() centers = [] new_edges = [] for e in edges: sv = Edge.StartVertex(e) ev = Edge.EndVertex(e) svx = Vertex.X(sv) svy = Vertex.Y(sv) evx = Vertex.X(ev) evy = Vertex.Y(ev) for x in xList: if abs(svx-x) < threshold: svx = x break; for y in yList: if abs(svy-y) < threshold: svy = y break; sv = Vertex.ByCoordinates(svx, svy, 0) for x in xList: if abs(evx-x) < threshold: evx = x break; for y in yList: if abs(evy-y) < threshold: evy = y break; sv = Vertex.ByCoordinates(svx, svy, 0) ev = Vertex.ByCoordinates(evx, evy, 0) new_edges.append(Edge.ByVertices([sv, ev], tolerance=tolerance)) cluster = Cluster.ByTopologies(new_edges) eb = Face.ByWire(Shell.ExternalBoundary(shell), tolerance=tolerance) shell = Topology.Slice(eb, cluster, tolerance=tolerance) if not Topology.IsInstance(shell, "Shell"): try: temp_wires = [Wire.RemoveCollinearEdges(w, angTolerance=1.0) for w in Topology.Wires(shell)] temp_faces = [Face.ByWire(w, tolerance=tolerance) for w in temp_wires] except: temp_faces = Topology.Faces(shell) shell = Shell.ByFaces(temp_faces, tolerance=tolerance) if transferDictionaries == True: selectors = [] for f in faces: d = Topology.Dictionary(f) s = Topology.InternalVertex(f, tolerance=tolerance) s = Topology.SetDictionary(s, d) selectors.append(s) _ = Topology.TransferDictionariesBySelectors(topology=shell, selectors=selectors, tranFaces=True, tolerance=tolerance) return shell return None
def ByFaces(faces: list, tolerance: float = 0.0001)
-
Creates a shell from the input list of faces.
Parameters
faces
:list
- The input list of faces.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Shell
- The created Shell.
Expand source code
@staticmethod def ByFaces(faces: list, tolerance: float = 0.0001): """ Creates a shell from the input list of faces. Parameters ---------- faces : list The input list of faces. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created Shell. """ from topologicpy.Topology import Topology if not isinstance(faces, list): return None faceList = [x for x in faces if Topology.IsInstance(x, "Face")] if len(faceList) == 0: print("Shell.ByFaces - Error: The input faces list does not contain any valid faces. Returning None.") return None shell = topologic.Shell.ByFaces(faceList, tolerance) # Hook to Core if not Topology.IsInstance(shell, "Shell"): shell = Topology.SelfMerge(shell, tolerance=tolerance) if Topology.IsInstance(shell, "Shell"): return shell else: print("Shell.ByFaces - Error: Could not create shell. Returning None.") return None else: return shell
def ByFacesCluster(cluster, tolerance: float = 0.0001)
-
Creates a shell from the input cluster of faces.
Parameters
cluster
:topologic_core.Cluster
- The input cluster of faces.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Shell
- The created shell.
Expand source code
@staticmethod def ByFacesCluster(cluster, tolerance: float = 0.0001): """ Creates a shell from the input cluster of faces. Parameters ---------- cluster : topologic_core.Cluster The input cluster of faces. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created shell. """ from topologicpy.Topology import Topology if not Topology.IsInstance(cluster, "Cluster"): return None faces = [] _ = cluster.Faces(None, faces) return Shell.ByFaces(faces, tolerance=tolerance)
def ByWires(wires: list, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False)
-
Creates a shell by lofting through the input wires Parameters
wires
:list
- The input list of wires.
triangulate
:bool
, optional- If set to True, the faces will be triangulated. The default is True.
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.Shell
- The creates shell.
Expand source code
@staticmethod def ByWires(wires: list, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False): """ Creates a shell by lofting through the input wires Parameters ---------- wires : list The input list of wires. triangulate : bool , optional If set to True, the faces will be triangulated. The default is True. 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.Shell The creates shell. """ from topologicpy.Edge import Edge from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology if not isinstance(wires, list): return None wireList = [x for x in wires if Topology.IsInstance(x, "Wire")] faces = [] for i in range(len(wireList)-1): wire1 = wireList[i] wire2 = wireList[i+1] if Topology.Type(wire1) < Topology.TypeID("Edge") or Topology.Type(wire2) < Topology.TypeID("Edge"): return None if Topology.Type(wire1) == Topology.TypeID("Edge"): w1_edges = [wire1] else: w1_edges = Topology.Edges(wire1) if Topology.Type(wire2) == Topology.TypeID("Edge"): w2_edges = [wire2] else: w2_edges = Topology.Edges(wire2) if len(w1_edges) != len(w2_edges): return None if triangulate == True: for j in range (len(w1_edges)): e1 = w1_edges[j] e2 = w2_edges[j] e3 = None e4 = None try: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) except: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e4], tolerance=tolerance), tolerance=tolerance)) try: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) except: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e3],tolerance=tolerance), tolerance=tolerance)) if e3 and e4: e5 = Edge.ByVertices([e1.StartVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) faces.append(Face.ByWire(Wire.ByEdges([e1, e5, e4], tolerance=tolerance), tolerance=tolerance)) faces.append(Face.ByWire(Wire.ByEdges([e2, e5, e3], tolerance=tolerance), tolerance=tolerance)) else: for j in range (len(w1_edges)): e1 = w1_edges[j] e2 = w2_edges[j] e3 = None e4 = None try: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) except: try: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) except: pass try: e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent) except: try: e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent) except: pass if e3 and e4: try: faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2, e3], tolerance=tolerance), tolerance=tolerance)) except: faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2, e4], tolerance=tolerance), tolerance=tolerance)) elif e3: faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2], tolerance=tolerance), tolerance=tolerance)) elif e4: faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2], tolerance=tolerance), tolerance=tolerance)) return Shell.ByFaces(faces, tolerance=tolerance)
def ByWiresCluster(cluster, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False)
-
Creates a shell by lofting through the input cluster of wires
Parameters
wires
:topologic_core.Cluster
- The input cluster of wires.
triangulate
:bool
, optional- If set to True, the faces will be triangulated. The default is True.
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.Shell
- The creates shell.
Expand source code
@staticmethod def ByWiresCluster(cluster, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False): """ Creates a shell by lofting through the input cluster of wires Parameters ---------- wires : topologic_core.Cluster The input cluster of wires. triangulate : bool , optional If set to True, the faces will be triangulated. The default is True. 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.Shell The creates shell. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not cluster: return None if not Topology.IsInstance(cluster, "Cluster"): return None wires = Cluster.Wires(cluster) return Shell.ByWires(wires, triangulate=triangulate, tolerance=tolerance, silent=silent)
def Circle(origin=None, radius: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, 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 32.
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.
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 pie. 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.Shell
- The created circle.
Expand source code
@staticmethod def Circle(origin= None, radius: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, 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 32. 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. 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 pie. 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.Shell The created circle. """ return Shell.Pie(origin=origin, radiusA=radius, radiusB=0, sides=sides, rings=1, fromAngle=fromAngle, toAngle=toAngle, direction=direction, placement=placement, tolerance=tolerance)
def Delaunay(vertices: list, face=None, tolerance: float = 0.0001)
-
Returns a delaunay partitioning of the input vertices. The vertices must be coplanar. See https://en.wikipedia.org/wiki/Delaunay_triangulation.
Parameters
vertices
:list
- The input list of vertices.
face
:topologic_core.Face
, optional- The input face. If specified, the delaunay triangulation is clipped to the face.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
shell
- A shell representing the delaunay triangulation of the input vertices.
Expand source code
@staticmethod def Delaunay(vertices: list, face= None, tolerance: float = 0.0001): """ Returns a delaunay partitioning of the input vertices. The vertices must be coplanar. See https://en.wikipedia.org/wiki/Delaunay_triangulation. Parameters ---------- vertices : list The input list of vertices. face : topologic_core.Face , optional The input face. If specified, the delaunay triangulation is clipped to the face. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- shell A shell representing the delaunay triangulation of the input vertices. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary from random import sample from scipy.spatial import Delaunay as SCIDelaunay import numpy as np if not isinstance(vertices, list): return None vertices = [x for x in vertices if Topology.IsInstance(x, "Vertex")] if len(vertices) < 3: return None # Create a Vertex at the world's origin (0, 0, 0) world_origin = Vertex.Origin() if Topology.IsInstance(face, "Face"): # Flatten the face origin = Topology.Centroid(face) normal = Face.Normal(face) flatFace = Topology.Flatten(face, origin=origin, direction=normal) faceVertices = Face.Vertices(face) vertices += faceVertices # Create a cluster of the input vertices verticesCluster = Cluster.ByTopologies(vertices) # Flatten the cluster using the same transformations verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal) vertices = Cluster.Vertices(verticesCluster) points = [] for v in vertices: points.append([Vertex.X(v), Vertex.Y(v)]) delaunay = SCIDelaunay(points) simplices = delaunay.simplices faces = [] for simplex in simplices: tempTriangleVertices = [] tempTriangleVertices.append(vertices[simplex[0]]) tempTriangleVertices.append(vertices[simplex[1]]) tempTriangleVertices.append(vertices[simplex[2]]) tempFace = Face.ByWire(Wire.ByVertices(tempTriangleVertices), tolerance=tolerance) faces.append(tempFace) shell = Shell.ByFaces(faces, tolerance=tolerance) if shell == None: shell = Cluster.ByTopologies(faces) if Topology.IsInstance(face, "Face"): edges = Topology.Edges(shell) shell = Topology.Slice(flatFace, Cluster.ByTopologies(edges)) # Get the internal boundaries of the face wires = Face.InternalBoundaries(flatFace) ibList = [] if len(wires) > 0: ibList = [Face.ByWire(w) for w in wires] cluster = Cluster.ByTopologies(ibList) shell = Topology.Difference(shell, cluster) shell = Topology.Unflatten(shell, origin=origin, direction=normal) return shell
def Edges(shell) ‑> list
-
Returns the edges of the input shell.
Parameters
shell
:topologic_core.Shell
- The input shell.
Returns
list
- The list of edges.
Expand source code
@staticmethod def Edges(shell) -> list: """ Returns the edges of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of edges. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None edges = [] _ = shell.Edges(None, edges) return edges
def ExternalBoundary(shell, tolerance: float = 0.0001)
-
Returns the external boundary of the input shell.
Parameters
shell
:topologic_core.Shell
- The input shell.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Wire
ortopologic_core.Cluster
- The external boundary of the input shell. If the shell has holes, the return value will be a cluster of wires.
Expand source code
@staticmethod def ExternalBoundary(shell, tolerance: float = 0.0001): """ Returns the external boundary of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Wire or topologic_core.Cluster The external boundary of the input shell. If the shell has holes, the return value will be a cluster of wires. """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None edges = [] _ = shell.Edges(None, edges) obEdges = [] for anEdge in edges: faces = [] _ = anEdge.Faces(shell, faces) if len(faces) == 1: obEdges.append(anEdge) return Topology.SelfMerge(Cluster.ByTopologies(obEdges), tolerance=tolerance)
def Faces(shell) ‑> list
-
Returns the faces of the input shell.
Parameters
shell
:topologic_core.Shell
- The input shell.
Returns
list
- The list of faces.
Expand source code
@staticmethod def Faces(shell) -> list: """ Returns the faces of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of faces. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None faces = [] _ = shell.Faces(None, faces) return faces
def HyperbolicParaboloidCircularDomain(origin=None, radius: float = 0.5, sides: int = 36, rings: int = 10, A: float = 2.0, B: float = -2.0, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a hyperbolic paraboloid with a circular domain. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
Parameters
origin
:topologic_core.Vertex
, optional- The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
radius
:float
, optional- The desired radius of the hyperbolic paraboloid. The default is 0.5.
sides
:int
, optional- The desired number of sides of the hyperbolic parabolid. The default is 36.
rings
:int
, optional- The desired number of concentric rings of the hyperbolic parabolid. The default is 10.
A
:float
, optional- The A constant in the equation z = Ax^2^ + By^2^. The default is 2.0.
B
:float
, optional- The B constant in the equation z = Ax^2^ + By^2^. The default is -2.0.
direction
:list
, optional- The vector representing the up direction of the hyperbolic paraboloid. 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", "bottom". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Shell
- The created hyperboloic paraboloid.
Expand source code
@staticmethod def HyperbolicParaboloidCircularDomain(origin= None, radius: float = 0.5, sides: int = 36, rings: int = 10, A: float = 2.0, B: float = -2.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a hyperbolic paraboloid with a circular domain. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape Parameters ---------- origin : topologic_core.Vertex , optional The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None. radius : float , optional The desired radius of the hyperbolic paraboloid. The default is 0.5. sides : int , optional The desired number of sides of the hyperbolic parabolid. The default is 36. rings : int , optional The desired number of concentric rings of the hyperbolic parabolid. The default is 10. A : float , optional The *A* constant in the equation z = A*x^2^ + B*y^2^. The default is 2.0. B : float , optional The *B* constant in the equation z = A*x^2^ + B*y^2^. The default is -2.0. direction : list , optional The vector representing the up direction of the hyperbolic paraboloid. 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", "bottom". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created hyperboloic paraboloid. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(origin, "Vertex"): origin = Vertex.ByCoordinates(0, 0, 0) uOffset = float(360)/float(sides) vOffset = float(radius)/float(rings) faces = [] for i in range(rings-1): r1 = radius - vOffset*i r2 = radius - vOffset*(i+1) for j in range(sides-1): a1 = math.radians(uOffset)*j a2 = math.radians(uOffset)*(j+1) x1 = math.sin(a1)*r1 y1 = math.cos(a1)*r1 z1 = A*x1*x1 + B*y1*y1 x2 = math.sin(a1)*r2 y2 = math.cos(a1)*r2 z2 = A*x2*x2 + B*y2*y2 x3 = math.sin(a2)*r2 y3 = math.cos(a2)*r2 z3 = A*x3*x3 + B*y3*y3 x4 = math.sin(a2)*r1 y4 = math.cos(a2)*r1 z4 = A*x4*x4 + B*y4*y4 v1 = Vertex.ByCoordinates(x1,y1,z1) v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) v4 = Vertex.ByCoordinates(x4,y4,z4) f1 = Face.ByVertices([v1,v2,v4]) f2 = Face.ByVertices([v4,v2,v3]) faces.append(f1) faces.append(f2) a1 = math.radians(uOffset)*(sides-1) a2 = math.radians(360) x1 = math.sin(a1)*r1 y1 = math.cos(a1)*r1 z1 = A*x1*x1 + B*y1*y1 x2 = math.sin(a1)*r2 y2 = math.cos(a1)*r2 z2 = A*x2*x2 + B*y2*y2 x3 = math.sin(a2)*r2 y3 = math.cos(a2)*r2 z3 = A*x3*x3 + B*y3*y3 x4 = math.sin(a2)*r1 y4 = math.cos(a2)*r1 z4 = A*x4*x4 + B*y4*y4 v1 = Vertex.ByCoordinates(x1,y1,z1) v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) v4 = Vertex.ByCoordinates(x4,y4,z4) f1 = Face.ByVertices([v1,v2,v4]) f2 = Face.ByVertices([v4,v2,v3]) faces.append(f1) faces.append(f2) # Special Case: Center triangles r = vOffset x1 = 0 y1 = 0 z1 = 0 v1 = Vertex.ByCoordinates(x1,y1,z1) for j in range(sides-1): a1 = math.radians(uOffset)*j a2 = math.radians(uOffset)*(j+1) x2 = math.sin(a1)*r y2 = math.cos(a1)*r z2 = A*x2*x2 + B*y2*y2 #z2 = 0 x3 = math.sin(a2)*r y3 = math.cos(a2)*r z3 = A*x3*x3 + B*y3*y3 #z3 = 0 v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) f1 = Face.ByVertices([v2,v1,v3]) faces.append(f1) a1 = math.radians(uOffset)*(sides-1) a2 = math.radians(360) x2 = math.sin(a1)*r y2 = math.cos(a1)*r z2 = A*x2*x2 + B*y2*y2 x3 = math.sin(a2)*r y3 = math.cos(a2)*r z3 = A*x3*x3 + B*y3*y3 v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) f1 = Face.ByVertices([v2,v1,v3]) faces.append(f1) returnTopology = Shell.ByFaces(faces, tolerance) if not returnTopology: returnTopology = Cluster.ByTopologies(faces) vertices = [] _ = returnTopology.Vertices(None, vertices) xList = [] yList = [] zList = [] for aVertex in vertices: xList.append(aVertex.X()) yList.append(aVertex.Y()) zList.append(aVertex.Z()) minX = min(xList) maxX = max(xList) minY = min(yList) maxY = max(yList) minZ = min(zList) maxZ = max(zList) xOffset = 0 yOffset = 0 zOffset = 0 if placement.lower() == "lowerleft": xOffset = -minX yOffset = -minY zOffset = -minZ elif placement.lower() == "bottom": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -minZ elif placement.lower() == "center": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -(minZ + (maxZ - minZ)*0.5) returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset) returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin) returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction) return returnTopology
def HyperbolicParaboloidRectangularDomain(origin=None, llVertex=None, lrVertex=None, ulVertex=None, urVertex=None, uSides: int = 10, vSides: int = 10, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a hyperbolic paraboloid with a rectangular domain.
Parameters
origin
:topologic_core.Vertex
, optional- The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
llVertex
:topologic_core.Vertex
, optional- The lower left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, -0.5, -0.5).
lrVertex
:topologic_core.Vertex
, optional- The lower right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, -0.5, 0.5).
ulVertex
:topologic_core.Vertex
, optional- The upper left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, 0.5, 0.5).
urVertex
:topologic_core.Vertex
, optional- The upper right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, 0.5, -0.5).
uSides
:int
, optional- The number of segments along the X axis. The default is 10.
vSides
:int
, optional- The number of segments along the Y axis. The default is 10.
direction
:list
, optional- The vector representing the up direction of the hyperbolic parabolid. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the hyperbolic parabolid. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Shell
- The created hyperbolic paraboloid.
Expand source code
@staticmethod def HyperbolicParaboloidRectangularDomain(origin= None, llVertex= None, lrVertex= None, ulVertex= None, urVertex= None, uSides: int = 10, vSides: int = 10, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a hyperbolic paraboloid with a rectangular domain. Parameters ---------- origin : topologic_core.Vertex , optional The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None. llVertex : topologic_core.Vertex , optional The lower left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, -0.5, -0.5). lrVertex : topologic_core.Vertex , optional The lower right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, -0.5, 0.5). ulVertex : topologic_core.Vertex , optional The upper left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, 0.5, 0.5). urVertex : topologic_core.Vertex , optional The upper right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, 0.5, -0.5). uSides : int , optional The number of segments along the X axis. The default is 10. vSides : int , optional The number of segments along the Y axis. The default is 10. direction : list , optional The vector representing the up direction of the hyperbolic parabolid. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the hyperbolic parabolid. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created hyperbolic paraboloid. """ from topologicpy.Vertex import Vertex from topologicpy.Edge import Edge from topologicpy.Face import Face from topologicpy.Topology import Topology if not Topology.IsInstance(origin, "Vertex"): origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(llVertex, "Vertex"): llVertex = Vertex.ByCoordinates(-0.5, -0.5, -0.5) if not Topology.IsInstance(lrVertex, "Vertex"): lrVertex = Vertex.ByCoordinates(0.5, -0.5, 0.5) if not Topology.IsInstance(ulVertex, "Vertex"): ulVertex = Vertex.ByCoordinates(-0.5, 0.5, 0.5) if not Topology.IsInstance(urVertex, "Vertex"): urVertex = Vertex.ByCoordinates(0.5, 0.5, -0.5) e1 = Edge.ByVertices([llVertex, lrVertex], tolerance=tolerance) e3 = Edge.ByVertices([urVertex, ulVertex], tolerance=tolerance) edges = [] for i in range(uSides+1): v1 = Edge.VertexByParameter(e1, float(i)/float(uSides)) v2 = Edge.VertexByParameter(e3, 1.0 - float(i)/float(uSides)) edges.append(Edge.ByVertices([v1, v2], tolerance=tolerance)) faces = [] for i in range(uSides): for j in range(vSides): v1 = Edge.VertexByParameter(edges[i], float(j)/float(vSides)) v2 = Edge.VertexByParameter(edges[i], float(j+1)/float(vSides)) v3 = Edge.VertexByParameter(edges[i+1], float(j+1)/float(vSides)) v4 = Edge.VertexByParameter(edges[i+1], float(j)/float(vSides)) faces.append(Face.ByVertices([v1, v2, v4])) faces.append(Face.ByVertices([v4, v2, v3])) returnTopology = Shell.ByFaces(faces, tolerance=tolerance) if not returnTopology: returnTopology = None xOffset = 0 yOffset = 0 zOffset = 0 minX = min([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()]) maxX = max([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()]) minY = min([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()]) maxY = max([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()]) minZ = min([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()]) maxZ = max([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()]) if placement.lower() == "lowerleft": xOffset = -minX yOffset = -minY zOffset = -minZ elif placement.lower() == "bottom": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -minZ elif placement.lower() == "center": xOffset = -(minX + (maxX - minX)*0.5) yOffset = -(minY + (maxY - minY)*0.5) zOffset = -(minZ + (maxZ - minZ)*0.5) returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset) returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin) returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction) return returnTopology
def InternalBoundaries(shell, tolerance=0.0001)
-
Returns the internal boundaries (closed wires) of the input shell. Internal boundaries are considered holes.
Parameters
shell
:topologic_core.Shell
- The input shell.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
list
- The list of internal boundaries
Expand source code
@staticmethod def InternalBoundaries(shell, tolerance=0.0001): """ Returns the internal boundaries (closed wires) of the input shell. Internal boundaries are considered holes. Parameters ---------- shell : topologic_core.Shell The input shell. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- list The list of internal boundaries """ from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology edges = [] _ = shell.Edges(None, edges) ibEdges = [] for anEdge in edges: faces = [] _ = anEdge.Faces(shell, faces) if len(faces) > 1: ibEdges.append(anEdge) returnTopology = Topology.SelfMerge(Cluster.ByTopologies(ibEdges), tolerance=tolerance) wires = Topology.Wires(returnTopology) return wires
def IsClosed(shell) ‑> bool
-
Returns True if the input shell is closed. Returns False otherwise.
Parameters
shell
:topologic_core.Shell
- The input shell.
Returns
bool
- True if the input shell is closed. False otherwise.
Expand source code
@staticmethod def IsClosed(shell) -> bool: """ Returns True if the input shell is closed. Returns False otherwise. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- bool True if the input shell is closed. False otherwise. """ return shell.IsClosed()
def IsOnBoundary(shell, vertex, tolerance: float = 0.0001) ‑> bool
-
Returns True if the input vertex is on the boundary of the input shell. Returns False otherwise. On the boundary is defined as being on the boundary of one of the shell's external or internal boundaries
Parameters
shell
:topologic_core.Shell
- The input shell.
vertex
:topologic_core.Vertex
- The input vertex.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
bool
- Returns True if the input vertex is inside the input shell. Returns False otherwise.
Expand source code
@staticmethod def IsOnBoundary(shell, vertex, tolerance: float = 0.0001) -> bool: """ Returns True if the input vertex is on the boundary of the input shell. Returns False otherwise. On the boundary is defined as being on the boundary of one of the shell's external or internal boundaries Parameters ---------- shell : topologic_core.Shell The input shell. vertex : topologic_core.Vertex The input vertex. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- bool Returns True if the input vertex is inside the input shell. Returns False otherwise. """ from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None if not Topology.IsInstance(vertex, "Vertex"): return None boundary = Shell.ExternalBoundary(shell, tolerance=tolerance) if Vertex.IsInternal(vertex, boundary, tolerance=tolerance): return True internal_boundaries = Shell.InternalBoundaries(shell, tolerance=tolerance) for ib in internal_boundaries: if Vertex.IsInternal(vertex, ib, tolerance=tolerance): return True return False
def Pie(origin=None, radiusA: float = 0.5, radiusB: float = 0.0, sides: int = 32, rings: int = 1, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)
-
Creates a pie shape.
Parameters
origin
:topologic_core.Vertex
, optional- The location of the origin of the pie. The default is None which results in the pie being placed at (0, 0, 0).
radiusA
:float
, optional- The outer radius of the pie. The default is 0.5.
radiusB
:float
, optional- The inner radius of the pie. The default is 0.25.
sides
:int
, optional- The number of sides of the pie. The default is 32.
rings
:int
, optional- The number of rings of the pie. The default is 1.
fromAngle
:float
, optional- The angle in degrees from which to start creating the arc of the pie. The default is 0.
toAngle
:float
, optional- The angle in degrees at which to end creating the arc of the pie. The default is 360.
direction
:list
, optional- The vector representing the up direction of the pie. The default is [0, 0, 1].
placement
:str
, optional- The description of the placement of the origin of the pie. 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.Shell
- The created pie.
Expand source code
@staticmethod def Pie(origin= None, radiusA: float = 0.5, radiusB: float = 0.0, sides: int = 32, rings: int = 1, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001): """ Creates a pie shape. Parameters ---------- origin : topologic_core.Vertex , optional The location of the origin of the pie. The default is None which results in the pie being placed at (0, 0, 0). radiusA : float , optional The outer radius of the pie. The default is 0.5. radiusB : float , optional The inner radius of the pie. The default is 0.25. sides : int , optional The number of sides of the pie. The default is 32. rings : int , optional The number of rings of the pie. The default is 1. fromAngle : float , optional The angle in degrees from which to start creating the arc of the pie. The default is 0. toAngle : float , optional The angle in degrees at which to end creating the arc of the pie. The default is 360. direction : list , optional The vector representing the up direction of the pie. The default is [0, 0, 1]. placement : str , optional The description of the placement of the origin of the pie. 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.Shell The created pie. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None if toAngle < fromAngle: toAngle += 360 if abs(toAngle-fromAngle) < tolerance: return None fromAngle = math.radians(fromAngle) toAngle = math.radians(toAngle) angleRange = toAngle - fromAngle radiusA = abs(radiusA) radiusB = abs(radiusB) if radiusB > radiusA: temp = radiusA radiusA = radiusB radiusB = temp if abs(radiusA - radiusB) < tolerance or radiusA < tolerance: return None radiusRange = radiusA - radiusB sides = int(abs(math.floor(sides))) if sides < 3: return None rings = int(abs(rings)) if radiusB < tolerance: radiusB = 0 xOffset = 0 yOffset = 0 zOffset = 0 if placement.lower() == "lowerleft": xOffset = radiusA yOffset = radiusA uOffset = float(angleRange)/float(sides) vOffset = float(radiusRange)/float(rings) faces = [] if radiusB > tolerance: for i in range(rings): r1 = radiusA - vOffset*i r2 = radiusA - vOffset*(i+1) for j in range(sides): a1 = fromAngle + uOffset*j a2 = fromAngle + uOffset*(j+1) x1 = math.sin(a1)*r1 y1 = math.cos(a1)*r1 z1 = 0 x2 = math.sin(a1)*r2 y2 = math.cos(a1)*r2 z2 = 0 x3 = math.sin(a2)*r2 y3 = math.cos(a2)*r2 z3 = 0 x4 = math.sin(a2)*r1 y4 = math.cos(a2)*r1 z4 = 0 v1 = Vertex.ByCoordinates(x1,y1,z1) v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) v4 = Vertex.ByCoordinates(x4,y4,z4) f1 = Face.ByVertices([v1,v2,v3,v4]) faces.append(f1) else: x1 = 0 y1 = 0 z1 = 0 v1 = Vertex.ByCoordinates(x1,y1,z1) for j in range(sides): a1 = fromAngle + uOffset*j a2 = fromAngle + uOffset*(j+1) x2 = math.sin(a1)*radiusA y2 = math.cos(a1)*radiusA z2 = 0 x3 = math.sin(a2)*radiusA y3 = math.cos(a2)*radiusA z3 = 0 v2 = Vertex.ByCoordinates(x2,y2,z2) v3 = Vertex.ByCoordinates(x3,y3,z3) f1 = Face.ByVertices([v2,v1,v3]) faces.append(f1) shell = Shell.ByFaces(faces, tolerance=tolerance) if not shell: return None shell = Topology.Translate(shell, xOffset, yOffset, zOffset) shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin) shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction) return shell
def Planarize(shell, origin=None, mantissa: int = 6, tolerance: float = 0.0001)
-
Returns a planarized version of the input shell.
Parameters
shell
:topologic_core.Shell
- The input shell.
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 shell will be projected. If set to None, the centroid of the input shell will be chosen. The default is None.
mantissa
:int
, optional- The desired length of the mantissa. The default is 6.
Returns
topologic_core.Shell
- The planarized shell.
Expand source code
@staticmethod def Planarize(shell, origin= None, mantissa: int = 6, tolerance: float = 0.0001): """ Returns a planarized version of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. 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 shell will be projected. If set to None, the centroid of the input shell will be chosen. The default is None. mantissa : int , optional The desired length of the mantissa. The default is 6. Returns ------- topologic_core.Shell The planarized shell. """ from topologicpy.Vertex import Vertex from topologicpy.Face import Face from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): print("Shell.Planarize - Error: The input wire parameter is not a valid topologic shell. Returning None.") return None if origin == None: origin = Vertex.Origin() if not Topology.IsInstance(origin, "Vertex"): print("Shell.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.") return None vertices = Topology.Vertices(shell) faces = Topology.Faces(shell) 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_shell = Topology.ReplaceVertices(shell, verticesA=vertices, verticesB=new_vertices) new_faces = Topology.Faces(new_shell) return Topology.SelfMerge(Cluster.ByTopologies(new_faces), tolerance=tolerance)
def Rectangle(origin=None, width: float = 1.0, length: float = 1.0, uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1], placement: str = 'center', 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.
uSides
:int
, optional- The number of sides along the width. The default is 2.
vSides
:int
, optional- The number of sides along the length. The default is 2.
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", or "lowerleft". It is case insensitive. The default is "center".
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
topologic_core.Shell
- The created shell.
Expand source code
@staticmethod def Rectangle(origin= None, width: float = 1.0, length: float = 1.0, uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1], placement: str = "center", 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. uSides : int , optional The number of sides along the width. The default is 2. vSides : int , optional The number of sides along the length. The default is 2. 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", or "lowerleft". It is case insensitive. The default is "center". tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- topologic_core.Shell The created shell. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology if not origin: origin = Vertex.ByCoordinates(0, 0, 0) if not Topology.IsInstance(origin, "Vertex"): return None uOffset = float(width)/float(uSides) vOffset = float(length)/float(vSides) faces = [] if placement.lower() == "center": wOffset = width*0.5 lOffset = length*0.5 else: wOffset = 0 lOffset = 0 for i in range(uSides): for j in range(vSides): rOrigin = Vertex.ByCoordinates(i*uOffset - wOffset, j*vOffset - lOffset, 0) w = Wire.Rectangle(origin=rOrigin, width=uOffset, length=vOffset, direction=[0, 0, 1], placement="lowerleft", tolerance=tolerance) f = Face.ByWire(w, tolerance=tolerance) faces.append(f) shell = Shell.ByFaces(faces, tolerance=tolerance) shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin) shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction) return shell
def RemoveCollinearEdges(shell, angTolerance: float = 0.1, tolerance: float = 0.0001)
-
Removes any collinear edges in the input shell.
Parameters
shell
:topologic_core.Shell
- The input shell.
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.Shell
- The created shell without any collinear edges.
Expand source code
@staticmethod def RemoveCollinearEdges(shell, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Removes any collinear edges in the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. 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.Shell The created shell without any collinear edges. """ from topologicpy.Face import Face from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): print("Shell.RemoveCollinearEdges - Error: The input shell parameter is not a valid shell. Returning None.") return None faces = Shell.Faces(shell) clean_faces = [] for face in faces: clean_faces.append(Face.RemoveCollinearEdges(face, angTolerance=angTolerance, tolerance=tolerance)) return Shell.ByFaces(clean_faces, tolerance=tolerance)
def Roof(face, angle: float = 45, epsilon: float = 0.01, 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.
epsilon
:float
, optional- The desired epsilon (another form of tolerance for distance from plane). The default is 0.01. (This is set to a larger number as it was found to work better)
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.Shell
- The created roof.
Expand source code
@staticmethod def Roof(face, angle: float = 45, epsilon: float = 0.01, 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. epsilon : float , optional The desired epsilon (another form of tolerance for distance from plane). The default is 0.01. (This is set to a larger number as it was found to work better) 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.Shell The created roof. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Shell import Shell from topologicpy.Cell import Cell from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary import topologic_core as topologic import math def nearest_vertex_2d(v, vertices, tolerance=0.001): for vertex in vertices: x2 = Vertex.X(vertex) y2 = Vertex.Y(vertex) temp_v = Vertex.ByCoordinates(x2, y2, Vertex.Z(v)) if Vertex.Distance(v, temp_v) <= tolerance: return vertex return None if not Topology.IsInstance(face, "Face"): return None angle = abs(angle) if angle >= 90-tolerance: return None if angle < tolerance: return None origin = Topology.Centroid(face) normal = Face.Normal(face) flat_face = Topology.Flatten(face, origin=origin, direction=normal) roof = Wire.Roof(flat_face, angle=angle, tolerance=tolerance) if not roof: return None shell = Shell.Skeleton(flat_face, tolerance=tolerance) faces = Shell.Faces(shell) Topology.Show(shell) if not faces: return None triangles = [] for face in faces: internalBoundaries = Face.InternalBoundaries(face) if len(internalBoundaries) == 0: if len(Topology.Vertices(face)) > 3: triangles += Face.Triangulate(face, tolerance=tolerance) else: triangles += [face] roof_vertices = Topology.Vertices(roof) flat_vertices = [] for rv in roof_vertices: flat_vertices.append(Vertex.ByCoordinates(Vertex.X(rv), Vertex.Y(rv), 0)) final_triangles = [] for triangle in triangles: if len(Topology.Vertices(triangle)) > 3: triangles = Face.Triangulate(triangle, tolerance=tolerance) else: triangles = [triangle] final_triangles += triangles final_faces = [] for triangle in final_triangles: face_vertices = Topology.Vertices(triangle) top_vertices = [] for sv in face_vertices: temp = nearest_vertex_2d(sv, roof_vertices, tolerance=tolerance) if temp: top_vertices.append(temp) else: top_vertices.append(sv) tri_face = Face.ByVertices(top_vertices) final_faces.append(tri_face) shell = Shell.ByFaces(final_faces, tolerance=tolerance) if not shell: shell = Cluster.ByTopologies(final_faces) try: shell = Topology.RemoveCoplanarFaces(shell, epsilon=epsilon, tolerance=tolerance) except: pass return shell
def SelfMerge(shell, angTolerance: float = 0.1, tolerance: float = 0.0001)
-
Creates a face by merging the faces of the input shell. The shell must be planar within the input angular tolerance.
Parameters
shell
:topologic_core.Shell
- The input shell.
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.Face
- The created face.
Expand source code
@staticmethod def SelfMerge(shell, angTolerance: float = 0.1, tolerance: float = 0.0001): """ Creates a face by merging the faces of the input shell. The shell must be planar within the input angular tolerance. Parameters ---------- shell : topologic_core.Shell The input shell. 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.Face The created face. """ from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Shell import Shell from topologicpy.Topology import Topology def planarizeList(wireList): returnList = [] for aWire in wireList: returnList.append(Wire.Planarize(aWire)) return returnList if not Topology.IsInstance(shell, "Shell"): return None ext_boundary = Shell.ExternalBoundary(shell, tolerance=tolerance) if Topology.IsInstance(ext_boundary, "Wire"): f = Face.ByWire(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance) or Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance)) if not f: print("FaceByPlanarShell - Error: The input Wire is not planar and could not be fixed. Returning None.") return None else: return f elif Topology.IsInstance(ext_boundary, "Cluster"): wires = [] _ = ext_boundary.Wires(None, wires) faces = [] areas = [] for aWire in wires: try: aFace = Face.ByWire(Topology.RemoveCollinearEdges(aWire, angTolerance)) except: aFace = Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(aWire, angTolerance))) anArea = Face.Area(aFace) faces.append(aFace) areas.append(anArea) max_index = areas.index(max(areas)) ext_boundary = faces[max_index] int_boundaries = list(set(faces) - set([ext_boundary])) int_wires = [] for int_boundary in int_boundaries: temp_wires = [] _ = int_boundary.Wires(None, temp_wires) int_wires.append(Topology.RemoveCollinearEdges(temp_wires[0], angTolerance)) temp_wires = [] _ = ext_boundary.Wires(None, temp_wires) ext_wire = Topology.RemoveCollinearEdges(temp_wires[0], angTolerance) try: return Face.ByWires(ext_wire, int_wires, tolerance=tolerance) except: return Face.ByWires(Wire.Planarize(ext_wire), planarizeList(int_wires), tolerance=tolerance) else: return None
def Simplify(shell, simplifyBoundary=True, tolerance=0.0001)
-
Simplifies the input shell 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
shell
:topologic_core.Shell
- The input shell.
simplifyBoundary
:bool
, optional- If set to True, the external boundary of the shell will be simplified as well. Otherwise, it will not be simplified. The default is True.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed.
Returns
topologic_core.Shell
- The simplified shell.
Expand source code
@staticmethod def Simplify(shell, simplifyBoundary=True, tolerance=0.0001): """ Simplifies the input shell 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 ---------- shell : topologic_core.Shell The input shell. simplifyBoundary : bool , optional If set to True, the external boundary of the shell will be simplified as well. Otherwise, it will not be simplified. The default is True. tolerance : float , optional The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed. Returns ------- topologic_core.Shell The simplified shell. """ from topologicpy.Vertex import Vertex from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Shell import Shell from topologicpy.Cluster import Cluster from topologicpy.Topology import Topology from topologicpy.Helper import Helper 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(shell, "Shell"): print("Shell.Simplify - Error: The input shell parameter is not a valid topologic shell. Returning None.") return None # Get the external boundary of the shell. This can be simplified as well, but might cause issues at the end. # At this point, it is assumed to be left as is. all_edges = Topology.Edges(shell) if simplifyBoundary == False: ext_boundary = Face.ByWire(Shell.ExternalBoundary(shell, tolerance=tolerance), tolerance=tolerance) # Get the internal edges of the shell. i_edges = [] for edge in all_edges: faces = Topology.SuperTopologies(edge, shell, topologyType="face") if len(faces) > 1: # This means that the edge separates two faces so it is internal. i_edges.append(edge) # Creat a Wire from the internal edges wire = Topology.SelfMerge(Cluster.ByTopologies(i_edges), tolerance=tolerance) else: wire = Topology.SelfMerge(Cluster.ByTopologies(all_edges), tolerance=tolerance) # Split the wires at its junctions (where more than two edges meet at a vertex) components = Wire.Split(wire) separators = [] wires = [] for component in components: if Topology.IsInstance(component, "Cluster"): component = Topology.SelfMerge(component, tolerance=tolerance) if Topology.IsInstance(component, "Cluster"): separators.append(Cluster.FreeEdges(component, tolerance=tolerance)) wires.append(Cluster.FreeWires(component, tolerance=tolerance)) if Topology.IsInstance(component, "Edge"): separators.append(component) if Topology.IsInstance(component, "Wire"): wires.append(component) if Topology.IsInstance(component, "Edge"): separators.append(component) if Topology.IsInstance(component, "Wire"): wires.append(component) wires = Helper.Flatten(wires) separators = Helper.Flatten(separators) results = [] for w in wires: temp_wire = Wire.ByVertices(douglas_peucker(w, tolerance), close=False) results.append(temp_wire) # Make a Cluster out of the results cluster = Cluster.ByTopologies(results) # Get all the edges of the result edges = Topology.Edges(cluster) # Add them to the final edges final_edges = edges + separators # Make a Cluster out of the final set of edges cluster = Cluster.ByTopologies(final_edges) if simplifyBoundary == False: # Slice the external boundary of the shell by the cluster final_result = Topology.Slice(ext_boundary, cluster, tolerance=tolerance) else: br = Wire.BoundingRectangle(shell) br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1.5) br = Face.ByWire(br, tolerance=tolerance) v = Face.VertexByParameters(br, 0.1, 0.1) result = Topology.Slice(br, cluster, tolerance=tolerance) faces = Topology.Faces(result) final_faces = [] for face in faces: if not Vertex.IsInternal(v, face, tolerance=0.01): final_faces.append(face) final_result = Shell.ByFaces(final_faces, tolerance=tolerance) return final_result
def Vertices(shell) ‑> list
-
Returns the vertices of the input shell.
Parameters
shell
:topologic_core.Shell
- The input shell.
Returns
list
- The list of vertices.
Expand source code
@staticmethod def Vertices(shell) -> list: """ Returns the vertices of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of vertices. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None vertices = [] _ = shell.Vertices(None, vertices) return vertices
def Voronoi(vertices: list, face=None, tolerance: float = 0.0001)
-
Returns a voronoi partitioning of the input face based on the input vertices. The vertices must be coplanar and within the face. See https://en.wikipedia.org/wiki/Voronoi_diagram.
Parameters
vertices
:list
- The input list of vertices.
face
:topologic_core.Face
, optional- The input face. If the face is not set an optimised bounding rectangle of the input vertices is used instead. The default is None.
tolerance
:float
, optional- The desired tolerance. The default is 0.0001.
Returns
shell
- A shell representing the voronoi partitioning of the input face.
Expand source code
@staticmethod def Voronoi(vertices: list, face= None, tolerance: float = 0.0001): """ Returns a voronoi partitioning of the input face based on the input vertices. The vertices must be coplanar and within the face. See https://en.wikipedia.org/wiki/Voronoi_diagram. Parameters ---------- vertices : list The input list of vertices. face : topologic_core.Face , optional The input face. If the face is not set an optimised bounding rectangle of the input vertices is used instead. The default is None. tolerance : float , optional The desired tolerance. The default is 0.0001. Returns ------- shell A shell representing the voronoi partitioning of the input face. """ 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.Dictionary import Dictionary if not Topology.IsInstance(face, "Face"): cluster = Cluster.ByTopologies(vertices) br = Wire.BoundingRectangle(cluster, optimize=5) face = Face.ByWire(br, tolerance=tolerance) if not isinstance(vertices, list): return None vertices = [x for x in vertices if Topology.IsInstance(x, "Vertex")] if len(vertices) < 2: return None # Flatten the input face origin = Topology.Centroid(face) normal = Face.Normal(face) flatFace = Topology.Flatten(face, origin=origin, direction=normal) eb = Face.ExternalBoundary(flatFace) ibList = Face.InternalBoundaries(flatFace) temp_verts = Topology.Vertices(eb) new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts] eb = Wire.ByVertices(new_verts, close=True) new_ibList = [] for ib in ibList: temp_verts = Topology.Vertices(ib) new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts] new_ibList.append(Wire.ByVertices(new_verts, close=True)) flatFace = Face.ByWires(eb, new_ibList) # Create a cluster of the input vertices verticesCluster = Cluster.ByTopologies(vertices) # Flatten the cluster using the same transformations verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal) flatVertices = Topology.Vertices(verticesCluster) flatVertices = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in flatVertices] points = [] for flatVertex in flatVertices: points.append([flatVertex.X(), flatVertex.Y()]) br = Wire.BoundingRectangle(flatFace) br_vertices = Wire.Vertices(br) br_x = [] br_y = [] for br_v in br_vertices: x, y = Vertex.Coordinates(br_v, outputType="xy") br_x.append(x) br_y.append(y) min_x = min(br_x) max_x = max(br_x) min_y = min(br_y) max_y = max(br_y) br_width = abs(max_x - min_x) br_length = abs(max_y - min_y) points.append((-br_width*4, -br_length*4)) points.append((-br_width*4, br_length*4)) points.append((br_width*4, -br_length*4)) points.append((br_width*4, br_length*4)) voronoi = Voronoi(points, furthest_site=False) voronoiVertices = [] for v in voronoi.vertices: voronoiVertices.append(Vertex.ByCoordinates(v[0], v[1], 0)) faces = [] for region in voronoi.regions: tempWire = [] if len(region) > 1 and not -1 in region: for v in region: tempWire.append(Vertex.ByCoordinates(voronoiVertices[v].X(), voronoiVertices[v].Y(), 0)) temp_verts = [] for v in tempWire: if len(temp_verts) == 0: temp_verts.append(v) elif Vertex.Index(v, temp_verts) == None: temp_verts.append(v) tempWire = temp_verts temp_w = Wire.ByVertices(tempWire, close=True) faces.append(Face.ByWire(Wire.ByVertices(tempWire, close=True), tolerance=tolerance)) shell = Shell.ByFaces(faces, tolerance=tolerance) edges = Shell.Edges(shell) edgesCluster = Cluster.ByTopologies(edges) shell = Topology.Slice(flatFace,edgesCluster, tolerance=tolerance) shell = Topology.Unflatten(shell, origin=origin, direction=normal) return shell
def Wires(shell) ‑> list
-
Returns the wires of the input shell.
Parameters
shell
:topologic_core.Shell
- The input shell.
Returns
list
- The list of wires.
Expand source code
@staticmethod def Wires(shell) -> list: """ Returns the wires of the input shell. Parameters ---------- shell : topologic_core.Shell The input shell. Returns ------- list The list of wires. """ from topologicpy.Topology import Topology if not Topology.IsInstance(shell, "Shell"): return None wires = [] _ = shell.Wires(None, wires) return wires
Methods
def Skeleton(face, tolerance: float = 0.001)
-
Creates a shell 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.
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.Shell
- The created straight skeleton.
Expand source code
def Skeleton(face, tolerance: float = 0.001): """ Creates a shell 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. 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.Shell The created straight skeleton. """ from topologicpy.Wire import Wire from topologicpy.Face import Face from topologicpy.Topology import Topology import topologic_core as topologic import math if not Topology.IsInstance(face, "Face"): return None roof = Wire.Skeleton(face, tolerance=tolerance) if not roof: return None br = Wire.BoundingRectangle(roof) #This works even if it is a Cluster not a Wire br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1) bf = Face.ByWire(br, tolerance=tolerance) large_shell = Topology.Boolean(bf, roof, operation="slice", tolerance=tolerance) if not large_shell: return None faces = Topology.Faces(large_shell) if not faces: return None final_faces = [] for f in faces: internalBoundaries = Face.InternalBoundaries(f) if len(internalBoundaries) == 0: final_faces.append(f) shell = Shell.ByFaces(final_faces, tolerance=tolerance) return shell