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 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, list(normal_end_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
- edge_direction = Edge.Direction(edge)
1168
- x, y, z = edge_direction
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
- sv = Vertex.ByCoordinates(normal_line_start)
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
- normal_edge = Topology.Rotate(normal_edge, origin=Edge.StartVertex(normal_edge), axis=[x,y,z], angle=angle)
1177
- dist = Edge.Length(edge)*u
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