topologicpy 0.8.61__py3-none-any.whl → 0.8.72__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 CHANGED
@@ -14,66 +14,188 @@
14
14
  # You should have received a copy of the GNU Affero General Public License along with
15
15
  # this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
- import topologic_core as topologic
18
- import warnings
17
+ from __future__ import annotations
18
+ from dataclasses import dataclass
19
+ from typing import List, Tuple, Optional, Callable, Any, Iterable
20
+ import math
19
21
 
22
+ # TopologicPy imports (used defensively to keep this file standalone-friendly)
20
23
  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.")
24
+ from topologicpy.Vertex import Vertex
25
+ from topologicpy.Edge import Edge
26
+ from topologicpy.Face import Face
27
+ from topologicpy.Topology import Topology
28
+ except Exception:
29
+ # If TopologicPy isn't present in the current environment, we still allow type checking.
30
+ Vertex = Edge = Face = Topology = object # type: ignore
33
31
 
32
+
33
+ # ----------------------------
34
+ # Axis-Aligned Bounding Box
35
+ # ----------------------------
36
+ @dataclass
37
+ class AABB:
38
+ """Axis-aligned bounding box: [minx,miny,minz]..[maxx,maxy,maxz]."""
39
+ minx: float; miny: float; minz: float
40
+ maxx: float; maxy: float; maxz: float
41
+
42
+ @staticmethod
43
+ def from_points(pts: Iterable[Tuple[float, float, float]], pad: float = 0.0) -> "AABB":
44
+ it = iter(pts)
45
+ try:
46
+ x, y, z = next(it)
47
+ except StopIteration:
48
+ # Empty: return a degenerate box at origin
49
+ return AABB(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
50
+ minx = maxx = float(x)
51
+ miny = maxy = float(y)
52
+ minz = maxz = float(z)
53
+ for x, y, z in it:
54
+ if x < minx: minx = x
55
+ if x > maxx: maxx = x
56
+ if y < miny: miny = y
57
+ if y > maxy: maxy = y
58
+ if z < minz: minz = z
59
+ if z > maxz: maxz = z
60
+ if pad:
61
+ minx -= pad; miny -= pad; minz -= pad
62
+ maxx += pad; maxy += pad; maxz += pad
63
+ return AABB(minx, miny, minz, maxx, maxy, maxz)
64
+
65
+ @staticmethod
66
+ def union(a: "AABB", b: "AABB") -> "AABB":
67
+ return AABB(
68
+ min(a.minx, b.minx), min(a.miny, b.miny), min(a.minz, b.minz),
69
+ max(a.maxx, b.maxx), max(a.maxy, b.maxy), max(a.maxz, b.maxz),
70
+ )
71
+
72
+ def extent(self) -> Tuple[float, float, float]:
73
+ return (self.maxx - self.minx, self.maxy - self.miny, self.maxz - self.minz)
74
+
75
+ def center(self) -> Tuple[float, float, float]:
76
+ return ((self.minx + self.maxx) * 0.5, (self.miny + self.maxy) * 0.5, (self.minz + self.maxz) * 0.5)
77
+
78
+ def overlaps(self, other: "AABB") -> bool:
79
+ return not (self.maxx < other.minx or self.minx > other.maxx or
80
+ self.maxy < other.miny or self.miny > other.maxy or
81
+ self.maxz < other.minz or self.minz > other.maxz)
82
+
83
+ def contains_point(self, p: Tuple[float, float, float]) -> bool:
84
+ x, y, z = p
85
+ return (self.minx <= x <= self.maxx and
86
+ self.miny <= y <= self.maxy and
87
+ self.minz <= z <= self.maxz)
88
+
89
+ def ray_intersect(self, ro: Tuple[float, float, float], rd: Tuple[float, float, float]) -> Tuple[bool, float, float]:
90
+ """Ray-box intersection using the 'slab' method.
91
+ Returns (hit, tmin, tmax) in ray param t, where point = ro + t*rd."""
92
+ (ox, oy, oz) = ro
93
+ (dx, dy, dz) = rd
94
+ tmin = -math.inf
95
+ tmax = math.inf
96
+
97
+ def axis(o, d, mn, mx, tmin, tmax):
98
+ if abs(d) < 1e-15:
99
+ # Ray parallel to slab: reject if origin not within slab
100
+ if o < mn or o > mx:
101
+ return False, tmin, tmax
102
+ return True, tmin, tmax
103
+ invD = 1.0 / d
104
+ t0 = (mn - o) * invD
105
+ t1 = (mx - o) * invD
106
+ if t0 > t1:
107
+ t0, t1 = t1, t0
108
+ tmin = max(tmin, t0)
109
+ tmax = min(tmax, t1)
110
+ if tmax < tmin:
111
+ return False, tmin, tmax
112
+ return True, tmin, tmax
113
+
114
+ ok, tmin, tmax = axis(ox, dx, self.minx, self.maxx, tmin, tmax)
115
+ if not ok: return (False, tmin, tmax)
116
+ ok, tmin, tmax = axis(oy, dy, self.miny, self.maxy, tmin, tmax)
117
+ if not ok: return (False, tmin, tmax)
118
+ ok, tmin, tmax = axis(oz, dz, self.minz, self.maxz, tmin, tmax)
119
+ if not ok: return (False, tmin, tmax)
120
+ return True, tmin, tmax
121
+
122
+
123
+ # ----------------------------
124
+ # BVH Node
125
+ # ----------------------------
126
+ @dataclass
127
+ class _BVHNode:
128
+ bbox: AABB
129
+ left: Optional[int] # index into nodes list
130
+ right: Optional[int] # index into nodes list
131
+ start: int # start index into items array (for leaves)
132
+ count: int # number of items (for leaves). If count>0, node is leaf.
133
+
134
+ def is_leaf(self) -> bool:
135
+ return self.count > 0
136
+
137
+
138
+ # ----------------------------
139
+ # BVH
140
+ # ----------------------------
34
141
  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
- if other == None:
45
- return False
46
- return np.all(self.min_point <= other.max_point) and np.all(self.max_point >= other.min_point)
47
-
48
- def contains(self, point):
49
- # Check if a point is contained within the AABB
50
- return np.all(self.min_point <= point) and np.all(self.max_point >= point)
51
-
52
- # MeshObject class that stores a reference to the Topologic object
53
- class MeshObject:
54
- def __init__(self, vertices, topologic_object):
55
- self.vertices = np.array(vertices)
56
- self.aabb = BVH.AABB(np.min(vertices, axis=0), np.max(vertices, axis=0))
57
- self.centroid = np.mean(vertices, axis=0)
58
- self.topologic_object = topologic_object # Store the Topologic object reference
59
-
60
- # BVH Node class
61
- class BVHNode:
62
- def __init__(self, aabb, left=None, right=None, objects=None):
63
- self.aabb = aabb
64
- self.left = left
65
- self.right = right
66
- self.objects = objects if objects else []
142
+ """
143
+ Basic Bounding Volume Hierarchy over TopologicPy topologies.
144
+
145
+ Usage:
146
+ # 1) Prepare your primitives (Faces, Edges, Cells, etc.)
147
+ faces = Topology.Faces(some_topology) # or any list of topologies
148
+
149
+ # 2) Build the BVH
150
+ bvh = BVH.FromTopologies(faces, max_leaf_size=4, pad=0.0, silent=False)
151
+
152
+ # 3) AABB query
153
+ hits = bvh.QueryAABB(AABB(minx, miny, minz, maxx, maxy, maxz))
154
+
155
+ # 4) Raycast (rough): returns candidate primitive indices
156
+ cand = bvh.Raycast((ox,oy,oz), (dx,dy,dz))
157
+
158
+ # 5) Nearest by centroid (coarse)
159
+ idx, dist = bvh.Nearest((x,y,z))
160
+ primitive = bvh.items[idx]
161
+ """
162
+
163
+ def __init__(self):
164
+ self.nodes: List[_BVHNode] = []
165
+ self.items: List[Any] = [] # original topologies
166
+ self.bboxes: List[AABB] = [] # per item bbox
167
+ self.centroids: List[Tuple[float, float, float]] = [] # per item centroid
168
+ self._root: Optional[int] = None
169
+
170
+ # ---------- Public API ----------
67
171
 
68
172
  @staticmethod
69
- def ByTopologies(*topologies, silent: bool = False):
173
+ def ByTopologies(
174
+ *topologies,
175
+ maxLeafSize: int = 4,
176
+ tolerance: float = 0.0001,
177
+ silent: bool = False
178
+ ) -> "BVH":
70
179
  """
71
180
  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.
72
181
 
73
182
  Parameters
74
183
  ----------
75
- topologies : list
76
- The list of topologies.
184
+ *topologies: (tuple of Topologic topologies)
185
+ One or more TopologicPy topologies to include in the BVH.
186
+ Each topology is automatically analyzed to extract its vertices and compute an axis-aligned bounding box (AABB)
187
+ for hierarchical spatial indexing.
188
+
189
+ maxLeafSize: int , optional
190
+ The maximum number of primitives (topologies) that can be stored in a single leaf node of the BVH.
191
+ Smaller values result in deeper trees with finer spatial subdivision (potentially faster queries but slower build times),
192
+ while larger values produce shallower trees with coarser spatial grouping (faster builds but less precise queries).
193
+ Default is 4.
194
+ tolerance : float , optional
195
+ The desired tolerance. Tolerance is used for an optional margin added to all sides of each topology's axis-aligned bounding box (AABB).
196
+ This helps account for numerical precision errors or slight geometric inaccuracies.
197
+ A small positive value ensures that closely adjacent or nearly touching primitives are
198
+ properly enclosed within their bounding boxes. Default is 0.0001.
77
199
  silent : bool , optional
78
200
  If set to True, error and warning messages are suppressed. Default is False.
79
201
 
@@ -87,214 +209,359 @@ class BVH:
87
209
  from topologicpy.Topology import Topology
88
210
  from topologicpy.Helper import Helper
89
211
 
90
- if len(topologies) == 0:
91
- print("BVH.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
92
- return None
93
- if len(topologies) == 1:
94
- topologyList = topologies[0]
95
- if isinstance(topologyList, list):
96
- if len(topologyList) == 0:
97
- if not silent:
98
- print("BVH.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
99
- return None
100
- else:
101
- topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
102
- if len(topologyList) == 0:
103
- if not silent:
104
- print("BVH.ByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
105
- return None
106
- else:
107
- if not silent:
108
- print("BVH.ByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
109
- return topologies
110
- else:
111
- topologyList = Helper.Flatten(list(topologies))
112
- topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
212
+ topologyList = Helper.Flatten(list(topologies))
213
+ topologyList = [t for t in topologyList if Topology.IsInstance(t, "Topology")]
214
+
113
215
  if len(topologyList) == 0:
114
216
  if not silent:
115
217
  print("BVH.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
116
218
  return None
117
- # Recursive BVH construction
118
- def build_bvh(objects, depth=0):
119
- if len(objects) == 1:
120
- return BVH.BVHNode(objects[0].aabb, objects=objects)
121
-
122
- # Split objects along the median axis based on their centroids
123
- axis = depth % 3
124
- objects.sort(key=lambda obj: obj.centroid[axis])
125
-
126
- mid = len(objects) // 2
127
- left_bvh = build_bvh(objects[:mid], depth + 1)
128
- right_bvh = build_bvh(objects[mid:], depth + 1)
129
-
130
- # Merge left and right bounding boxes
131
- combined_aabb = BVH.AABB(
132
- np.minimum(left_bvh.aabb.min_point, right_bvh.aabb.min_point),
133
- np.maximum(left_bvh.aabb.max_point, right_bvh.aabb.max_point)
134
- )
135
-
136
- return BVH.BVHNode(combined_aabb, left_bvh, right_bvh)
137
219
 
138
- mesh_objects = []
139
- for topology in topologyList:
140
- vertices = [(Vertex.X(v), Vertex.Y(v), Vertex.Z(v)) for v in Topology.Vertices(topology)]
141
- mesh_objects.append(BVH.MeshObject(vertices, topology))
220
+ bvh = BVH()
221
+
222
+ # Precompute per-item AABBs & centroids
223
+ bvh.items = topologyList
224
+ bvh.bboxes = []
225
+ bvh.centroids = []
226
+
227
+ for topo in bvh.items:
228
+ pts = [Vertex.Coordinates(v) for v in Topology.Vertices(topo)]
229
+ if not pts:
230
+ # Degenerate: keep a tiny box at (0,0,0) to avoid crashes
231
+ box = AABB.from_points([(0.0, 0.0, 0.0)], pad=tolerance)
232
+ c = (0.0, 0.0, 0.0)
233
+ else:
234
+ box = AABB.from_points(pts, pad=tolerance)
235
+ c = box.center()
236
+ bvh.bboxes.append(box)
237
+ bvh.centroids.append(c)
238
+
239
+ # Build using indices
240
+ indices = list(range(len(bvh.items)))
241
+ if not indices:
242
+ if not silent:
243
+ print("BVH.Topologies - Warning: no items to build.")
244
+ return bvh
245
+
246
+ # Reserve nodes list
247
+ bvh.nodes = []
248
+ bvh._root = bvh._build_recursive(indices, maxLeafSize)
249
+ if not silent:
250
+ depth = bvh.Depth(bvh)
251
+ print(f"BVH.ByTopologies - Information: Built with {len(bvh.items)} items, {len(bvh.nodes)} nodes, depth ~{depth}.")
252
+ return bvh
253
+
254
+ @staticmethod
255
+ def Depth(bvh) -> int:
256
+ """
257
+ Returns an approximate depth of the BVH.
142
258
 
143
- return build_bvh(mesh_objects)
259
+ Parameters
260
+ ----------
261
+ bvh : BVH
262
+ The bvh tree.
263
+
264
+ Returns
265
+ -------
266
+ int
267
+ The approximate depth of the input bvh tree.
268
+
269
+ """
270
+ def _depth(i: int) -> int:
271
+ n = bvh.nodes[i]
272
+ if n.is_leaf(): return 1
273
+ return 1 + max(_depth(n.left), _depth(n.right)) # type: ignore
274
+ if bvh._root is None: return 0
275
+ return _depth(bvh._root)
276
+
277
+ @staticmethod
278
+ def QueryAABB(bvh, query_box: AABB):
279
+ """Return indices of items whose AABBs overlap query_box."""
280
+ out: List[int] = []
281
+ if bvh._root is None: return out
282
+ stack = [bvh._root]
283
+ while stack:
284
+ ni = stack.pop()
285
+ node = bvh.nodes[ni]
286
+ if not node.bbox.overlaps(query_box):
287
+ continue
288
+ if node.is_leaf():
289
+ for k in range(node.start, node.start + node.count):
290
+ idx = bvh._leaf_items[k]
291
+ if bvh.bboxes[idx].overlaps(query_box):
292
+ out.append(idx)
293
+ else:
294
+ stack.append(node.left) # type: ignore
295
+ stack.append(node.right) # type: ignore
296
+ return out
144
297
 
145
298
  @staticmethod
146
- def QueryByTopologies(*topologies, silent: bool = False):
299
+ def Clashes(bvh, *topologies, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
147
300
  """
148
- 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.
149
-
301
+ Returns candidate primitives (topologies) overlapping the BVH (AABB-level) of the input topologies list.
302
+ You can follow up with precise TopologicPy geometry intersection if needed.
303
+
150
304
  Parameters
151
305
  ----------
152
- topologies : list
153
- The list of topologies.
306
+ bvh : BVH
307
+ The bvh tree.
308
+ *topologies: (tuple of Topologic topologies)
309
+ One or more TopologicPy topologies to include in the BVH.
310
+ Each topology is automatically analyzed to extract its vertices and compute an axis-aligned bounding box (AABB)
311
+ for hierarchical spatial indexing.
312
+ mantissa : int , optional
313
+ The desired length of the mantissa. Default is 6.
314
+ tolerance : float , optional
315
+ The desired tolerance. Default is 0.0001. Tolerance is used for an optional margin added to all sides of each topology's axis-aligned bounding box (AABB).
316
+ This helps account for numerical precision errors or slight geometric inaccuracies.
317
+ A small positive value ensures that closely adjacent or nearly touching primitives are
318
+ properly enclosed within their bounding boxes. Default is 0.0001.
154
319
  silent : bool , optional
155
320
  If set to True, error and warning messages are suppressed. Default is False.
156
321
 
157
322
  Returns
158
323
  -------
159
- BVH query
160
- The created BVH query.
324
+ list
325
+ The list of topologies that broadly interest the input list of topologies.
161
326
 
162
327
  """
163
328
  from topologicpy.Vertex import Vertex
164
- from topologicpy.Cluster import Cluster
329
+ from topologicpy.Cell import Cell
165
330
  from topologicpy.Topology import Topology
166
- from topologicpy.Dictionary import Dictionary
167
331
  from topologicpy.Helper import Helper
168
332
 
169
- if len(topologies) == 0:
170
- print("BVH.QueryByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
171
- return None
172
- if len(topologies) == 1:
173
- topologyList = topologies[0]
174
- if isinstance(topologyList, list):
175
- if len(topologyList) == 0:
176
- if not silent:
177
- print("BVH.QueryByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
178
- return None
179
- else:
180
- topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
181
- if len(topologyList) == 0:
182
- if not silent:
183
- print("BVH.QueryByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
184
- return None
185
- else:
186
- if not silent:
187
- print("BVH.QueryByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
188
- return topologies
189
- else:
190
- topologyList = Helper.Flatten(list(topologies))
191
- topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
333
+ topologyList = Helper.Flatten(list(topologies))
334
+ topologyList = [t for t in topologyList if Topology.IsInstance(t, "Topology")]
335
+
192
336
  if len(topologyList) == 0:
193
337
  if not silent:
194
- print("BVH.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
338
+ print("BVH.Clashes - Error: The input parameters do not contain any valid topologies. Returning None.")
195
339
  return None
196
- vertices = []
340
+
341
+ return_topologies = []
197
342
  for topology in topologyList:
198
- if Topology.IsInstance(topology, "Vertex"):
199
- vertices.append(topology)
343
+ if Topology.IsInstance(topology, "vertex"):
344
+ x,y,z = Vertex.Coordinates(topology, mantissa=mantissa)
345
+ points = [[x-tolerance, y-tolerance, z-tolerance], [x+tolerance, y+tolerance, z+tolerance]]
200
346
  else:
201
- vertices.extend(Topology.Vertices(topology))
202
- cluster = Cluster.ByTopologies(vertices)
203
- bb = Topology.BoundingBox(cluster)
204
- d = Topology.Dictionary(bb)
205
- x_min = Dictionary.ValueAtKey(d, "xmin")
206
- y_min = Dictionary.ValueAtKey(d, "ymin")
207
- z_min = Dictionary.ValueAtKey(d, "zmin")
208
- x_max = Dictionary.ValueAtKey(d, "zmax")
209
- y_max = Dictionary.ValueAtKey(d, "ymax")
210
- z_max = Dictionary.ValueAtKey(d, "zmax")
211
- query_aabb = BVH.AABB(min_point=(x_min, y_min, z_min), max_point=(x_max, y_max, z_max))
212
- return query_aabb
213
-
214
- def Clashes(bvh, query):
215
- """
216
- Returns a list of topologies in the input bvh tree that clashes (broad phase) with the list of topologies in the input query.
347
+ points = [Vertex.Coordinates(v, mantissa=mantissa) for v in Topology.Vertices(topology)]
348
+ aabb_box = AABB.from_points(points, pad = tolerance)
349
+ return_topologies.extend([bvh.items[i] for i in BVH.QueryAABB(bvh, aabb_box)])
350
+ return return_topologies
217
351
 
352
+ @staticmethod
353
+ def Raycast(bvh, origin, direction: Tuple[float, float, float], mantissa: int = 6, silent: bool = False) -> List[int]:
354
+ """
355
+ Returns candidate primitives intersecting the BVH (AABB-level).
356
+ You can follow up with precise TopologicPy geometry intersection if needed.
357
+
218
358
  Parameters
219
359
  ----------
220
- topologies : list
221
- The list of topologies.
360
+ bvh : BVH
361
+ The bvh tree.
362
+ origin : topologic_core.Vertex
363
+ The origin of the ray vector
364
+ direction : topologic_core.Vector
365
+ The direction of the raycast vector.
366
+ mantissa : int , optional
367
+ The desired length of the mantissa. Default is 6.
222
368
  silent : bool , optional
223
369
  If set to True, error and warning messages are suppressed. Default is False.
224
370
 
225
371
  Returns
226
372
  -------
227
373
  list
228
- The list of clashing topologies (based on their axis-aligned bounding box (AABB))
374
+ The list of the indices of the possible candidates interesecting the input ray vector.
229
375
 
230
376
  """
231
- # Function to perform clash detection (broad-phase) and return Topologic objects
232
- def clash_detection(bvh_node, query_aabb, clashing_objects=None):
233
- if clashing_objects is None:
234
- clashing_objects = []
235
-
236
- # Check if the query AABB intersects with the current node's AABB
237
- if not bvh_node.aabb.intersects(query_aabb):
238
- return clashing_objects
239
-
240
- # If this is a leaf node, check each object in the node
241
- if bvh_node.objects:
242
- for obj in bvh_node.objects:
243
- if obj.aabb.intersects(query_aabb):
244
- clashing_objects.append(obj.topologic_object) # Return the Topologic object
245
- return clashing_objects
246
-
247
- # Recursively check the left and right child nodes
248
- clash_detection(bvh_node.left, query_aabb, clashing_objects)
249
- clash_detection(bvh_node.right, query_aabb, clashing_objects)
250
-
251
- return clashing_objects
252
- return clash_detection(bvh, query)
253
-
254
- # Function to recursively add nodes and edges to the TopologicPy Graph
255
- def Graph(bvh, tolerance: float = 0.0001, silent: bool = False):
256
- """
257
- Creates a graph from the input bvh tree.
258
377
 
378
+ from topologicpy.Vertex import Vertex
379
+ from topologicpy.Topology import Topology
380
+
381
+ if not isinstance(bvh, BVH):
382
+ if not silent:
383
+ print("BVH.Raycast - Error: The input bvh parameter is not a valid BVH tree. Returning None.")
384
+ return None
385
+ if not Topology.IsInstance(origin, "vertex"):
386
+ if not silent:
387
+ print("BVH.Raycast - Error: The input origin parameter is not a valid topologic Vertex. Returning None.")
388
+ return None
389
+ if not isinstance(direction, list):
390
+ if not silent:
391
+ print("BVH.Raycast - Error: The input direction parameter is not a valid vector. Returning None.")
392
+ return None
393
+ if not len(direction) < 3:
394
+ if not silent:
395
+ print("BVH.Raycast - Error: The input direction parameter is not a valid vector. Returning None.")
396
+ return None
397
+ o_coords = Vertex.Coordinates(origin, mantissa=mantissa)
398
+ out: List[int] = []
399
+ if bvh._root is None:
400
+ if not silent:
401
+ print("BVH.Raycast - Warning: The input bvh parameter is empty. Returning an empty list.")
402
+ return out
403
+
404
+ # Normalize direction if possible (not strictly required)
405
+ dx, dy, dz = direction
406
+ mag = math.sqrt(dx*dx + dy*dy + dz*dz)
407
+ if mag > 0:
408
+ direction = (dx/mag, dy/mag, dz/mag)
409
+
410
+ stack = [bvh._root]
411
+ while stack:
412
+ ni = stack.pop()
413
+ node = bvh.nodes[ni]
414
+ hit, tmin, tmax = node.bbox.ray_intersect(o_coords, direction)
415
+ if not hit or tmax < 0:
416
+ continue
417
+ if node.is_leaf():
418
+ for k in range(node.start, node.start + node.count):
419
+ idx = bvh._leaf_items[k]
420
+ h2, _, _ = bvh.bboxes[idx].ray_intersect(o_coords, direction)
421
+ if h2:
422
+ out.append(idx)
423
+ else:
424
+ stack.append(node.left) # type: ignore
425
+ stack.append(node.right) # type: ignore
426
+ return out
427
+
428
+ @staticmethod
429
+ def Nearest(bvh, vertex, mantissa: int = 6, silent: bool = False):
430
+ """
431
+ Returns the topology with centroid nearest to the input vertex.
432
+ Uses AABB distance lower-bounds to prune search.
259
433
  Parameters
260
434
  ----------
261
- bvh : BVH Tree
262
- The input BVH Tree.
263
- tolerance : float , optional
264
- The desired tolerance. Default is 0.0001.
435
+ bvh : BVH
436
+ The bvh tree.
437
+ vertex : topologic_core.Vertex
438
+ The input vertex.
439
+ mantissa : int , optional
440
+ The desired length of the mantissa. Default is 6.
265
441
  silent : bool , optional
266
442
  If set to True, error and warning messages are suppressed. Default is False.
267
443
 
268
444
  Returns
269
445
  -------
270
- topologic_core.Graph
271
- The created graph.
272
-
446
+ topologic_core.Topology
447
+ The topology with centroid nearest to the input vertex.
273
448
  """
449
+
274
450
  from topologicpy.Vertex import Vertex
275
- from topologicpy.Edge import Edge
276
- from topologicpy.Graph import Graph
277
451
  from topologicpy.Topology import Topology
278
- import random
279
- def add_bvh_to_graph(bvh_node, graph, parent_vertex=None, tolerance=0.0001):
280
- # Create a vertex for the current node's AABB centroid
281
- centroid = bvh_node.aabb.centroid
282
- current_vertex = Vertex.ByCoordinates(x=centroid[0], y=centroid[1], z=centroid[2])
283
-
284
- # Add an edge from the parent to this vertex (if a parent exists)
285
- if parent_vertex is not None:
286
- d = Vertex.Distance(parent_vertex, current_vertex)
287
- if d <= tolerance:
288
- current_vertex = Topology.Translate(current_vertex, tolerance*random.uniform(2,50), tolerance*random.uniform(2,50), tolerance*random.uniform(2,50))
289
- edge = Edge.ByVertices(parent_vertex, current_vertex, tolerance=tolerance, silent=silent)
290
- graph = Graph.AddEdge(graph, edge, silent=silent)
291
-
292
- # Recursively add child nodes
293
- if bvh_node.left:
294
- graph = add_bvh_to_graph(bvh_node.left, graph, parent_vertex=current_vertex, tolerance=tolerance)
295
- if bvh_node.right:
296
- graph = add_bvh_to_graph(bvh_node.right, graph, parent_vertex=current_vertex, tolerance=tolerance)
297
-
298
- return graph
299
- graph = Graph.ByVerticesEdges([Vertex.Origin()], [])
300
- return add_bvh_to_graph(bvh, graph, parent_vertex = None, tolerance=tolerance)
452
+
453
+ if not isinstance(bvh, BVH):
454
+ if not silent:
455
+ print("BVH.Nearest - Error: The input bvh parameter is not a valid BVH tree. Returning None.")
456
+ return None
457
+ if not Topology.IsInstance(vertex, "vertex"):
458
+ if not silent:
459
+ print("BVH.Nearest - Error: The input vertex parameter is not a valid topologic Vertex. Returning None.")
460
+ return None
461
+ if bvh._root is None or not bvh.items:
462
+ if not silent:
463
+ print("BVH.Nearest - Warning: The input bhv tree is empty. Returning None.")
464
+ return None
465
+
466
+ best_idx = -1
467
+ best_d2 = float("inf")
468
+
469
+ def d2_point_aabb(p: Tuple[float, float, float], b: AABB) -> float:
470
+ px, py, pz = p
471
+ dx = 0.0
472
+ if px < b.minx: dx = b.minx - px
473
+ elif px > b.maxx: dx = px - b.maxx
474
+ dy = 0.0
475
+ if py < b.miny: dy = b.miny - py
476
+ elif py > b.maxy: dy = py - b.maxy
477
+ dz = 0.0
478
+ if pz < b.minz: dz = b.minz - pz
479
+ elif pz > b.maxz: dz = pz - b.maxz
480
+ return dx*dx + dy*dy + dz*dz
481
+
482
+ stack = [bvh._root]
483
+ point = Vertex.Coordinates(vertex, mantissa=mantissa)
484
+ while stack:
485
+ ni = stack.pop()
486
+ node = bvh.nodes[ni]
487
+ if d2_point_aabb(point, node.bbox) >= best_d2:
488
+ continue
489
+ if node.is_leaf():
490
+ for k in range(node.start, node.start + node.count):
491
+ idx = bvh._leaf_items[k]
492
+ cx, cy, cz = bvh.centroids[idx]
493
+ dx = cx - point[0]; dy = cy - point[1]; dz = cz - point[2]
494
+ d2 = dx*dx + dy*dy + dz*dz
495
+ if d2 < best_d2:
496
+ best_d2 = d2
497
+ best_idx = idx
498
+ else:
499
+ # Visit child likely nearer first to improve pruning
500
+ l = bvh.nodes[node.left] # type: ignore
501
+ r = bvh.nodes[node.right] # type: ignore
502
+ dl = d2_point_aabb(point, l.bbox)
503
+ dr = d2_point_aabb(point, r.bbox)
504
+ if dl < dr:
505
+ stack.append(node.right) # type: ignore
506
+ stack.append(node.left) # type: ignore
507
+ else:
508
+ stack.append(node.left) # type: ignore
509
+ stack.append(node.right) # type: ignore
510
+
511
+ return bvh.items[best_idx]
512
+
513
+ # ---------- Internal build ----------
514
+
515
+ def _build_recursive(self, indices: List[int], max_leaf_size: int) -> int:
516
+ """Builds a subtree for 'indices' and returns node index."""
517
+ # Compute node bbox and centroid bbox
518
+ node_bbox = self.bboxes[indices[0]]
519
+ cx_min = cx_max = self.centroids[indices[0]][0]
520
+ cy_min = cy_max = self.centroids[indices[0]][1]
521
+ cz_min = cz_max = self.centroids[indices[0]][2]
522
+ for i in indices[1:]:
523
+ node_bbox = AABB.union(node_bbox, self.bboxes[i])
524
+ cx, cy, cz = self.centroids[i]
525
+ if cx < cx_min: cx_min = cx
526
+ if cx > cx_max: cx_max = cx
527
+ if cy < cy_min: cy_min = cy
528
+ if cy > cy_max: cy_max = cy
529
+ if cz < cz_min: cz_min = cz
530
+ if cz > cz_max: cz_max = cz
531
+
532
+ if len(indices) <= max_leaf_size:
533
+ start = len(getattr(self, "_leaf_items", []))
534
+ if not hasattr(self, "_leaf_items"):
535
+ self._leaf_items: List[int] = []
536
+ self._leaf_items.extend(indices)
537
+ node = _BVHNode(node_bbox, None, None, start, len(indices))
538
+ self.nodes.append(node)
539
+ return len(self.nodes) - 1
540
+
541
+ # Choose split axis (longest centroid axis)
542
+ ex = cx_max - cx_min
543
+ ey = cy_max - cy_min
544
+ ez = cz_max - cz_min
545
+ if ex >= ey and ex >= ez:
546
+ axis = 0
547
+ elif ey >= ex and ey >= ez:
548
+ axis = 1
549
+ else:
550
+ axis = 2
551
+
552
+ # Median split by centroid along chosen axis
553
+ mid = len(indices) // 2
554
+ indices.sort(key=lambda i: self.centroids[i][axis])
555
+ left_idx = indices[:mid]
556
+ right_idx = indices[mid:]
557
+
558
+ # Handle pathological case (all centroids equal) by forcing a split
559
+ if not left_idx or not right_idx:
560
+ left_idx = indices[:len(indices)//2]
561
+ right_idx = indices[len(indices)//2:]
562
+
563
+ left_node = self._build_recursive(left_idx, max_leaf_size)
564
+ right_node = self._build_recursive(right_idx, max_leaf_size)
565
+ node = _BVHNode(node_bbox, left_node, right_node, start=0, count=0)
566
+ self.nodes.append(node)
567
+ return len(self.nodes) - 1