topologicpy 0.7.57__py3-none-any.whl → 0.7.60__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- topologicpy/BVH.py +296 -0
- topologicpy/Edge.py +40 -16
- topologicpy/Graph.py +124 -79
- topologicpy/Plotly.py +33 -6
- topologicpy/Topology.py +16 -0
- topologicpy/Vector.py +0 -1
- topologicpy/Wire.py +159 -60
- topologicpy/version.py +1 -1
- {topologicpy-0.7.57.dist-info → topologicpy-0.7.60.dist-info}/METADATA +1 -1
- {topologicpy-0.7.57.dist-info → topologicpy-0.7.60.dist-info}/RECORD +13 -12
- {topologicpy-0.7.57.dist-info → topologicpy-0.7.60.dist-info}/LICENSE +0 -0
- {topologicpy-0.7.57.dist-info → topologicpy-0.7.60.dist-info}/WHEEL +0 -0
- {topologicpy-0.7.57.dist-info → topologicpy-0.7.60.dist-info}/top_level.txt +0 -0
topologicpy/BVH.py
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
# Copyright (C) 2024
|
2
|
+
# Wassim Jabi <wassim.jabi@gmail.com>
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify it under
|
5
|
+
# the terms of the GNU Affero General Public License as published by the Free Software
|
6
|
+
# Foundation, either version 3 of the License, or (at your option) any later
|
7
|
+
# version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
10
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
11
|
+
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
12
|
+
# details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU Affero General Public License along with
|
15
|
+
# this program. If not, see <https://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
import topologic_core as topologic
|
18
|
+
import warnings
|
19
|
+
|
20
|
+
try:
|
21
|
+
import numpy as np
|
22
|
+
except:
|
23
|
+
print("BVH - Installing required numpy library.")
|
24
|
+
try:
|
25
|
+
os.system("pip install numpy")
|
26
|
+
except:
|
27
|
+
os.system("pip install numpy --user")
|
28
|
+
try:
|
29
|
+
import numpy as np
|
30
|
+
print("BVH - numpy library installed correctly.")
|
31
|
+
except:
|
32
|
+
warnings.warn("ANN - Error: Could not import numpy.")
|
33
|
+
|
34
|
+
class BVH:
|
35
|
+
# A class for Axis-Aligned Bounding Box (AABB)
|
36
|
+
class AABB:
|
37
|
+
def __init__(self, min_point, max_point):
|
38
|
+
self.min_point = np.array(min_point)
|
39
|
+
self.max_point = np.array(max_point)
|
40
|
+
self.centroid = (self.min_point + self.max_point) / 2.0
|
41
|
+
|
42
|
+
def intersects(self, other):
|
43
|
+
# Check if this AABB intersects with another AABB
|
44
|
+
return np.all(self.min_point <= other.max_point) and np.all(self.max_point >= other.min_point)
|
45
|
+
|
46
|
+
def contains(self, point):
|
47
|
+
# Check if a point is contained within the AABB
|
48
|
+
return np.all(self.min_point <= point) and np.all(self.max_point >= point)
|
49
|
+
|
50
|
+
# MeshObject class that stores a reference to the Topologic object
|
51
|
+
class MeshObject:
|
52
|
+
def __init__(self, vertices, topologic_object):
|
53
|
+
self.vertices = np.array(vertices)
|
54
|
+
self.aabb = BVH.AABB(np.min(vertices, axis=0), np.max(vertices, axis=0))
|
55
|
+
self.centroid = np.mean(vertices, axis=0)
|
56
|
+
self.topologic_object = topologic_object # Store the Topologic object reference
|
57
|
+
|
58
|
+
# BVH Node class
|
59
|
+
class BVHNode:
|
60
|
+
def __init__(self, aabb, left=None, right=None, objects=None):
|
61
|
+
self.aabb = aabb
|
62
|
+
self.left = left
|
63
|
+
self.right = right
|
64
|
+
self.objects = objects if objects else []
|
65
|
+
|
66
|
+
@staticmethod
|
67
|
+
def ByTopologies(*topologies, silent: bool = False):
|
68
|
+
"""
|
69
|
+
Creates a BVH Tree from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.
|
70
|
+
|
71
|
+
Parameters
|
72
|
+
----------
|
73
|
+
topologies : list
|
74
|
+
The list of topologies.
|
75
|
+
silent : bool , optional
|
76
|
+
If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
|
77
|
+
|
78
|
+
Returns
|
79
|
+
-------
|
80
|
+
BVH tree
|
81
|
+
The created BVH tree.
|
82
|
+
|
83
|
+
"""
|
84
|
+
from topologicpy.Vertex import Vertex
|
85
|
+
from topologicpy.Topology import Topology
|
86
|
+
from topologicpy.Helper import Helper
|
87
|
+
|
88
|
+
if len(topologies) == 0:
|
89
|
+
print("BVH.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
|
90
|
+
return None
|
91
|
+
if len(topologies) == 1:
|
92
|
+
topologyList = topologies[0]
|
93
|
+
if isinstance(topologyList, list):
|
94
|
+
if len(topologyList) == 0:
|
95
|
+
if not silent:
|
96
|
+
print("BVH.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
|
97
|
+
return None
|
98
|
+
else:
|
99
|
+
topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
|
100
|
+
if len(topologyList) == 0:
|
101
|
+
if not silent:
|
102
|
+
print("BVH.ByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
|
103
|
+
return None
|
104
|
+
else:
|
105
|
+
if not silent:
|
106
|
+
print("BVH.ByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
|
107
|
+
return topologies
|
108
|
+
else:
|
109
|
+
topologyList = Helper.Flatten(list(topologies))
|
110
|
+
topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
|
111
|
+
if len(topologyList) == 0:
|
112
|
+
if not silent:
|
113
|
+
print("BVH.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
|
114
|
+
return None
|
115
|
+
# Recursive BVH construction
|
116
|
+
def build_bvh(objects, depth=0):
|
117
|
+
if len(objects) == 1:
|
118
|
+
return BVH.BVHNode(objects[0].aabb, objects=objects)
|
119
|
+
|
120
|
+
# Split objects along the median axis based on their centroids
|
121
|
+
axis = depth % 3
|
122
|
+
objects.sort(key=lambda obj: obj.centroid[axis])
|
123
|
+
|
124
|
+
mid = len(objects) // 2
|
125
|
+
left_bvh = build_bvh(objects[:mid], depth + 1)
|
126
|
+
right_bvh = build_bvh(objects[mid:], depth + 1)
|
127
|
+
|
128
|
+
# Merge left and right bounding boxes
|
129
|
+
combined_aabb = BVH.AABB(
|
130
|
+
np.minimum(left_bvh.aabb.min_point, right_bvh.aabb.min_point),
|
131
|
+
np.maximum(left_bvh.aabb.max_point, right_bvh.aabb.max_point)
|
132
|
+
)
|
133
|
+
|
134
|
+
return BVH.BVHNode(combined_aabb, left_bvh, right_bvh)
|
135
|
+
|
136
|
+
mesh_objects = []
|
137
|
+
for topology in topologyList:
|
138
|
+
vertices = [(Vertex.X(v), Vertex.Y(v), Vertex.Z(v)) for v in Topology.Vertices(topology)]
|
139
|
+
mesh_objects.append(BVH.MeshObject(vertices, topology))
|
140
|
+
|
141
|
+
return build_bvh(mesh_objects)
|
142
|
+
|
143
|
+
@staticmethod
|
144
|
+
def QueryByTopologies(*topologies, silent: bool = False):
|
145
|
+
"""
|
146
|
+
Creates a BVH Query from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.
|
147
|
+
|
148
|
+
Parameters
|
149
|
+
----------
|
150
|
+
topologies : list
|
151
|
+
The list of topologies.
|
152
|
+
silent : bool , optional
|
153
|
+
If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
|
154
|
+
|
155
|
+
Returns
|
156
|
+
-------
|
157
|
+
BVH query
|
158
|
+
The created BVH query.
|
159
|
+
|
160
|
+
"""
|
161
|
+
from topologicpy.Vertex import Vertex
|
162
|
+
from topologicpy.Cluster import Cluster
|
163
|
+
from topologicpy.Topology import Topology
|
164
|
+
from topologicpy.Dictionary import Dictionary
|
165
|
+
from topologicpy.Helper import Helper
|
166
|
+
|
167
|
+
if len(topologies) == 0:
|
168
|
+
print("BVH.QueryByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
|
169
|
+
return None
|
170
|
+
if len(topologies) == 1:
|
171
|
+
topologyList = topologies[0]
|
172
|
+
if isinstance(topologyList, list):
|
173
|
+
if len(topologyList) == 0:
|
174
|
+
if not silent:
|
175
|
+
print("BVH.QueryByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
|
176
|
+
return None
|
177
|
+
else:
|
178
|
+
topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
|
179
|
+
if len(topologyList) == 0:
|
180
|
+
if not silent:
|
181
|
+
print("BVH.QueryByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
|
182
|
+
return None
|
183
|
+
else:
|
184
|
+
if not silent:
|
185
|
+
print("BVH.QueryByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
|
186
|
+
return topologies
|
187
|
+
else:
|
188
|
+
topologyList = Helper.Flatten(list(topologies))
|
189
|
+
topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
|
190
|
+
if len(topologyList) == 0:
|
191
|
+
if not silent:
|
192
|
+
print("BVH.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
|
193
|
+
return None
|
194
|
+
vertices = []
|
195
|
+
for topology in topologyList:
|
196
|
+
if Topology.IsInstance(topology, "Vertex"):
|
197
|
+
vertices.append(topology)
|
198
|
+
else:
|
199
|
+
vertices.extend(Topology.Vertices(topology))
|
200
|
+
cluster = Cluster.ByTopologies(vertices)
|
201
|
+
bb = Topology.BoundingBox(cluster)
|
202
|
+
d = Topology.Dictionary(bb)
|
203
|
+
min_x = Dictionary.ValueAtKey(d, "minx")
|
204
|
+
min_y = Dictionary.ValueAtKey(d, "miny")
|
205
|
+
min_z = Dictionary.ValueAtKey(d, "minz")
|
206
|
+
max_x = Dictionary.ValueAtKey(d, "maxx")
|
207
|
+
max_y = Dictionary.ValueAtKey(d, "maxy")
|
208
|
+
max_z = Dictionary.ValueAtKey(d, "maxz")
|
209
|
+
query_aabb = BVH.AABB(min_point=(min_x, min_y, min_z), max_point=(max_x,max_y,max_z))
|
210
|
+
return query_aabb
|
211
|
+
|
212
|
+
def Clashes(bvh, query):
|
213
|
+
"""
|
214
|
+
Returns a list of topologies in the input bvh tree that clashes (broad phase) with the list of topologies in the input query.
|
215
|
+
|
216
|
+
Parameters
|
217
|
+
----------
|
218
|
+
topologies : list
|
219
|
+
The list of topologies.
|
220
|
+
silent : bool , optional
|
221
|
+
If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
|
222
|
+
|
223
|
+
Returns
|
224
|
+
-------
|
225
|
+
list
|
226
|
+
The list of clashing topologies (based on their axis-aligned bounding box (AABB))
|
227
|
+
|
228
|
+
"""
|
229
|
+
# Function to perform clash detection (broad-phase) and return Topologic objects
|
230
|
+
def clash_detection(bvh_node, query_aabb, clashing_objects=None):
|
231
|
+
if clashing_objects is None:
|
232
|
+
clashing_objects = []
|
233
|
+
|
234
|
+
# Check if the query AABB intersects with the current node's AABB
|
235
|
+
if not bvh_node.aabb.intersects(query_aabb):
|
236
|
+
return clashing_objects
|
237
|
+
|
238
|
+
# If this is a leaf node, check each object in the node
|
239
|
+
if bvh_node.objects:
|
240
|
+
for obj in bvh_node.objects:
|
241
|
+
if obj.aabb.intersects(query_aabb):
|
242
|
+
clashing_objects.append(obj.topologic_object) # Return the Topologic object
|
243
|
+
return clashing_objects
|
244
|
+
|
245
|
+
# Recursively check the left and right child nodes
|
246
|
+
clash_detection(bvh_node.left, query_aabb, clashing_objects)
|
247
|
+
clash_detection(bvh_node.right, query_aabb, clashing_objects)
|
248
|
+
|
249
|
+
return clashing_objects
|
250
|
+
return clash_detection(bvh, query)
|
251
|
+
|
252
|
+
# Function to recursively add nodes and edges to the TopologicPy Graph
|
253
|
+
def Graph(bvh, tolerance=0.0001):
|
254
|
+
"""
|
255
|
+
Creates a graph from the input bvh tree.
|
256
|
+
|
257
|
+
Parameters
|
258
|
+
----------
|
259
|
+
bvh : BVH Tree
|
260
|
+
The input BVH Tree.
|
261
|
+
tolerance : float , optional
|
262
|
+
The desired tolerance. The default is 0.0001.
|
263
|
+
|
264
|
+
Returns
|
265
|
+
-------
|
266
|
+
topologic_core.Graph
|
267
|
+
The created graph.
|
268
|
+
|
269
|
+
"""
|
270
|
+
from topologicpy.Vertex import Vertex
|
271
|
+
from topologicpy.Edge import Edge
|
272
|
+
from topologicpy.Graph import Graph
|
273
|
+
from topologicpy.Topology import Topology
|
274
|
+
import random
|
275
|
+
def add_bvh_to_graph(bvh_node, graph, parent_vertex=None, tolerance=0.0001):
|
276
|
+
# Create a vertex for the current node's AABB centroid
|
277
|
+
centroid = bvh_node.aabb.centroid
|
278
|
+
current_vertex = Vertex.ByCoordinates(x=centroid[0], y=centroid[1], z=centroid[2])
|
279
|
+
|
280
|
+
# Add an edge from the parent to this vertex (if a parent exists)
|
281
|
+
if parent_vertex is not None:
|
282
|
+
d = Vertex.Distance(parent_vertex, current_vertex)
|
283
|
+
if d < tolerance:
|
284
|
+
current_vertex = Topology.Translate(current_vertex, tolerance*random.uniform(2,50), tolerance*random.uniform(2,50), tolerance*random.uniform(2,50))
|
285
|
+
edge = Edge.ByVertices(parent_vertex, current_vertex, tolerance=tolerance)
|
286
|
+
graph = Graph.AddEdge(graph, edge, silent=True)
|
287
|
+
|
288
|
+
# Recursively add child nodes
|
289
|
+
if bvh_node.left:
|
290
|
+
graph = add_bvh_to_graph(bvh_node.left, graph, parent_vertex=current_vertex, tolerance=tolerance)
|
291
|
+
if bvh_node.right:
|
292
|
+
graph = add_bvh_to_graph(bvh_node.right, graph, parent_vertex=current_vertex, tolerance=tolerance)
|
293
|
+
|
294
|
+
return graph
|
295
|
+
graph = Graph.ByVerticesEdges([Vertex.Origin()], [])
|
296
|
+
return add_bvh_to_graph(bvh, graph, parent_vertex = None, tolerance=tolerance)
|
topologicpy/Edge.py
CHANGED
@@ -290,6 +290,7 @@ class Edge():
|
|
290
290
|
"""
|
291
291
|
from topologicpy.Helper import Helper
|
292
292
|
from topologicpy.Topology import Topology
|
293
|
+
import inspect
|
293
294
|
|
294
295
|
if len(args) == 0:
|
295
296
|
print("Edge.ByVertices - Error: The input vertices parameter is an empty list. Returning None.")
|
@@ -322,6 +323,9 @@ class Edge():
|
|
322
323
|
if not edge:
|
323
324
|
if not silent:
|
324
325
|
print("Edge.ByVertices - Error: Could not create an edge. Returning None.")
|
326
|
+
curframe = inspect.currentframe()
|
327
|
+
calframe = inspect.getouterframes(curframe, 2)
|
328
|
+
print('caller name:', calframe[1][3])
|
325
329
|
return edge
|
326
330
|
|
327
331
|
@staticmethod
|
@@ -1135,17 +1139,22 @@ class Edge():
|
|
1135
1139
|
from topologicpy.Topology import Topology
|
1136
1140
|
|
1137
1141
|
def calculate_normal(start_vertex, end_vertex):
|
1138
|
-
start_vertex = [float(x) for x in start_vertex]
|
1139
|
-
end_vertex = [float(x) for x in end_vertex]
|
1140
|
-
# Calculate the direction vector of the line segment
|
1141
|
-
direction_vector = np.array(end_vertex) - np.array(start_vertex)
|
1142
|
-
|
1143
|
-
# Calculate the normal vector by swapping components and negating one of them
|
1144
|
-
normal_vector = np.array([-direction_vector[1], direction_vector[0], 0])
|
1145
|
-
|
1146
|
-
# Normalize the normal vector
|
1147
|
-
normal_vector /= norm(normal_vector)
|
1142
|
+
start_vertex = np.array([float(x) for x in start_vertex])
|
1143
|
+
end_vertex = np.array([float(x) for x in end_vertex])
|
1148
1144
|
|
1145
|
+
# Calculate the direction vector of the edge
|
1146
|
+
direction_vector = end_vertex - start_vertex
|
1147
|
+
|
1148
|
+
# Handle the horizontal edge case (no Z component)
|
1149
|
+
if np.isclose(direction_vector[2], 0):
|
1150
|
+
# The edge lies in the X-Y plane; compute a perpendicular in the X-Y plane
|
1151
|
+
normal_vector = np.array([-direction_vector[1], direction_vector[0], 0.0])
|
1152
|
+
else:
|
1153
|
+
# Otherwise, calculate the normal by crossing with the Z-axis
|
1154
|
+
z_axis = np.array([0, 0, 1])
|
1155
|
+
normal_vector = np.cross(direction_vector, z_axis)
|
1156
|
+
|
1157
|
+
normal_vector /= norm(normal_vector) # Normalize the normal vector
|
1149
1158
|
return normal_vector
|
1150
1159
|
|
1151
1160
|
def calculate_normal_line(start_vertex, end_vertex):
|
@@ -1156,7 +1165,7 @@ class Edge():
|
|
1156
1165
|
normal_end_vertex = np.array(start_vertex) + normal_vector
|
1157
1166
|
|
1158
1167
|
# Return the start and end vertices of the normal line
|
1159
|
-
return start_vertex,
|
1168
|
+
return start_vertex, normal_end_vertex
|
1160
1169
|
|
1161
1170
|
if not Topology.IsInstance(edge, "Edge"):
|
1162
1171
|
print("Edge.NormalEdge - Error: The input edge parameter is not a valid edge. Returning None.")
|
@@ -1164,18 +1173,33 @@ class Edge():
|
|
1164
1173
|
if length <= 0.0:
|
1165
1174
|
print("Edge.NormalEdge - Error: The input length parameter is not a positive number greater than zero. Returning None.")
|
1166
1175
|
return None
|
1167
|
-
|
1168
|
-
|
1176
|
+
|
1177
|
+
# Get start and end vertex coordinates
|
1169
1178
|
start_vertex = Vertex.Coordinates(Edge.StartVertex(edge))
|
1170
1179
|
end_vertex = Vertex.Coordinates(Edge.EndVertex(edge))
|
1180
|
+
|
1181
|
+
# Calculate the normal line
|
1171
1182
|
normal_line_start, normal_line_end = calculate_normal_line(start_vertex, end_vertex)
|
1172
|
-
|
1183
|
+
|
1184
|
+
# Create the normal edge in Topologic
|
1185
|
+
sv = Vertex.ByCoordinates(list(normal_line_start))
|
1173
1186
|
ev = Vertex.ByCoordinates(list(normal_line_end))
|
1187
|
+
|
1188
|
+
# Create an edge from the start to the end of the normal vector
|
1174
1189
|
normal_edge = Edge.ByVertices([sv, ev])
|
1190
|
+
|
1191
|
+
# Set the length of the normal edge
|
1175
1192
|
normal_edge = Edge.SetLength(normal_edge, length, bothSides=False)
|
1176
|
-
|
1177
|
-
|
1193
|
+
|
1194
|
+
# Rotate the normal edge around the input edge by the specified angle
|
1195
|
+
edge_direction = Edge.Direction(edge)
|
1196
|
+
x, y, z = edge_direction
|
1197
|
+
normal_edge = Topology.Rotate(normal_edge, origin=Edge.StartVertex(normal_edge), axis=[x, y, z], angle=angle)
|
1198
|
+
|
1199
|
+
# Translate the normal edge along the edge direction according to the u parameter
|
1200
|
+
dist = Edge.Length(edge) * u
|
1178
1201
|
normal_edge = Topology.TranslateByDirectionDistance(normal_edge, edge_direction, dist)
|
1202
|
+
|
1179
1203
|
return normal_edge
|
1180
1204
|
|
1181
1205
|
@staticmethod
|