topologicpy 0.8.61__py3-none-any.whl → 0.8.71__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 +478 -211
- topologicpy/Dictionary.py +17 -6
- topologicpy/Face.py +23 -50
- topologicpy/Shell.py +50 -14
- topologicpy/Topology.py +88 -82
- topologicpy/Vertex.py +333 -99
- topologicpy/version.py +1 -1
- {topologicpy-0.8.61.dist-info → topologicpy-0.8.71.dist-info}/METADATA +1 -1
- {topologicpy-0.8.61.dist-info → topologicpy-0.8.71.dist-info}/RECORD +12 -12
- {topologicpy-0.8.61.dist-info → topologicpy-0.8.71.dist-info}/WHEEL +0 -0
- {topologicpy-0.8.61.dist-info → topologicpy-0.8.71.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.61.dist-info → topologicpy-0.8.71.dist-info}/top_level.txt +0 -0
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
|
-
|
18
|
-
import
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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(
|
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
|
76
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
-
|
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
|
299
|
+
def Clashes(bvh, *topologies, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
|
147
300
|
"""
|
148
|
-
|
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
|
-
|
153
|
-
The
|
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
|
-
|
160
|
-
The
|
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.
|
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
|
-
|
170
|
-
|
171
|
-
|
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.
|
338
|
+
print("BVH.Clashes - Error: The input parameters do not contain any valid topologies. Returning None.")
|
195
339
|
return None
|
196
|
-
|
340
|
+
|
341
|
+
return_topologies = []
|
197
342
|
for topology in topologyList:
|
198
|
-
if Topology.IsInstance(topology, "
|
199
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
221
|
-
The
|
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
|
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
|
262
|
-
The
|
263
|
-
|
264
|
-
The
|
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.
|
271
|
-
The
|
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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
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
|