topologicpy 0.5.8__py3-none-any.whl → 6.0.0__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/Aperture.py +72 -72
- topologicpy/Cell.py +2169 -2169
- topologicpy/CellComplex.py +1137 -1137
- topologicpy/Cluster.py +1288 -1280
- topologicpy/Color.py +423 -393
- topologicpy/Context.py +79 -79
- topologicpy/DGL.py +3213 -3136
- topologicpy/Dictionary.py +698 -695
- topologicpy/Edge.py +1187 -1187
- topologicpy/EnergyModel.py +1180 -1171
- topologicpy/Face.py +2141 -2141
- topologicpy/Graph.py +7768 -7700
- topologicpy/Grid.py +353 -353
- topologicpy/Helper.py +507 -507
- topologicpy/Honeybee.py +461 -461
- topologicpy/Matrix.py +271 -271
- topologicpy/Neo4j.py +521 -521
- topologicpy/Plotly.py +2 -2
- topologicpy/Polyskel.py +541 -541
- topologicpy/Shell.py +1768 -1768
- topologicpy/Speckle.py +508 -508
- topologicpy/Topology.py +7060 -6988
- topologicpy/Vector.py +905 -905
- topologicpy/Vertex.py +1585 -1585
- topologicpy/Wire.py +3050 -3050
- topologicpy/__init__.py +22 -38
- topologicpy/version.py +1 -0
- {topologicpy-0.5.8.dist-info → topologicpy-6.0.0.dist-info}/LICENSE +661 -704
- topologicpy-6.0.0.dist-info/METADATA +751 -0
- topologicpy-6.0.0.dist-info/RECORD +32 -0
- topologicpy/bin/linux/topologic/__init__.py +0 -2
- topologicpy/bin/linux/topologic/libTKBO-6bdf205d.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKBRep-2960a069.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKBool-c44b74bd.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKFillet-9a670ba0.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKG2d-8f31849e.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKG3d-4c6bce57.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKMath-72572fa8.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKMesh-2a060427.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKOffset-6cab68ff.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKPrim-eb1262b3.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libgcc_s-32c1665e.so.1 +0 -0
- topologicpy/bin/linux/topologic/libstdc++-672d7b41.so.6.0.30 +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-310-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-311-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-38-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-39-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBO-6bdf205d.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBRep-2960a069.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBool-c44b74bd.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKFillet-9a670ba0.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKG2d-8f31849e.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKG3d-4c6bce57.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKMath-72572fa8.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKMesh-2a060427.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKOffset-6cab68ff.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKPrim-eb1262b3.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libgcc_s-32c1665e.so.1 +0 -0
- topologicpy/bin/linux/topologic.libs/libstdc++-672d7b41.so.6.0.30 +0 -0
- topologicpy/bin/macos/topologic/__init__.py +0 -2
- topologicpy/bin/windows/topologic/TKBO-f6b191de.dll +0 -0
- topologicpy/bin/windows/topologic/TKBRep-e56a600e.dll +0 -0
- topologicpy/bin/windows/topologic/TKBool-7b8d47ae.dll +0 -0
- topologicpy/bin/windows/topologic/TKFillet-0ddbf0a8.dll +0 -0
- topologicpy/bin/windows/topologic/TKG2d-2e2dee3d.dll +0 -0
- topologicpy/bin/windows/topologic/TKG3d-6674513d.dll +0 -0
- topologicpy/bin/windows/topologic/TKGeomAlgo-d240e370.dll +0 -0
- topologicpy/bin/windows/topologic/TKGeomBase-df87aba5.dll +0 -0
- topologicpy/bin/windows/topologic/TKMath-45bd625a.dll +0 -0
- topologicpy/bin/windows/topologic/TKMesh-d6e826b1.dll +0 -0
- topologicpy/bin/windows/topologic/TKOffset-79b9cc94.dll +0 -0
- topologicpy/bin/windows/topologic/TKPrim-aa430a86.dll +0 -0
- topologicpy/bin/windows/topologic/TKShHealing-bb48be89.dll +0 -0
- topologicpy/bin/windows/topologic/TKTopAlgo-7d0d1e22.dll +0 -0
- topologicpy/bin/windows/topologic/TKernel-08c8cfbb.dll +0 -0
- topologicpy/bin/windows/topologic/__init__.py +0 -2
- topologicpy/bin/windows/topologic/topologic.cp310-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp311-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp38-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp39-win_amd64.pyd +0 -0
- topologicpy-0.5.8.dist-info/METADATA +0 -96
- topologicpy-0.5.8.dist-info/RECORD +0 -91
- {topologicpy-0.5.8.dist-info → topologicpy-6.0.0.dist-info}/WHEEL +0 -0
- {topologicpy-0.5.8.dist-info → topologicpy-6.0.0.dist-info}/top_level.txt +0 -0
topologicpy/Vertex.py
CHANGED
@@ -1,1586 +1,1586 @@
|
|
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 topologicpy
|
18
|
-
import topologic
|
19
|
-
from topologicpy.Face import Face
|
20
|
-
from topologicpy.Topology import Topology
|
21
|
-
import collections
|
22
|
-
import os
|
23
|
-
import warnings
|
24
|
-
|
25
|
-
try:
|
26
|
-
import numpy as np
|
27
|
-
except:
|
28
|
-
print("Vertex - Installing required numpy library.")
|
29
|
-
try:
|
30
|
-
os.system("pip install numpy")
|
31
|
-
except:
|
32
|
-
os.system("pip install numpy --user")
|
33
|
-
try:
|
34
|
-
import numpy as np
|
35
|
-
print("Vertex - numpy library installed successfully.")
|
36
|
-
except:
|
37
|
-
warnings.warn("Vertex - Error: Could not import numpy.")
|
38
|
-
|
39
|
-
class Vertex(Topology):
|
40
|
-
@staticmethod
|
41
|
-
def AreCollinear(vertices: list, tolerance: float = 0.0001):
|
42
|
-
"""
|
43
|
-
Returns True if the input list of vertices form a straight line. Returns False otherwise.
|
44
|
-
|
45
|
-
Parameters
|
46
|
-
----------
|
47
|
-
vertices : list
|
48
|
-
The input list of vertices.
|
49
|
-
tolerance : float, optional
|
50
|
-
The desired tolerance. The default is 0.0001.
|
51
|
-
|
52
|
-
Returns
|
53
|
-
-------
|
54
|
-
bool
|
55
|
-
True if the input vertices are on the same side of the face. False otherwise.
|
56
|
-
|
57
|
-
"""
|
58
|
-
from topologicpy.Cluster import Cluster
|
59
|
-
from topologicpy.Topology import Topology
|
60
|
-
from topologicpy.Vector import Vector
|
61
|
-
import sys
|
62
|
-
def areCollinear(vertices, tolerance=0.0001):
|
63
|
-
point1 = [Vertex.X(vertices[0]), Vertex.Y(vertices[0]), Vertex.Z(vertices[0])]
|
64
|
-
point2 = [Vertex.X(vertices[1]), Vertex.Y(vertices[1]), Vertex.Z(vertices[1])]
|
65
|
-
point3 = [Vertex.X(vertices[2]), Vertex.Y(vertices[2]), Vertex.Z(vertices[2])]
|
66
|
-
|
67
|
-
vector1 = [point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]]
|
68
|
-
vector2 = [point3[0] - point1[0], point3[1] - point1[1], point3[2] - point1[2]]
|
69
|
-
|
70
|
-
cross_product_result = Vector.Cross(vector1, vector2, tolerance=tolerance)
|
71
|
-
return cross_product_result == None
|
72
|
-
|
73
|
-
if not isinstance(vertices, list):
|
74
|
-
print("Vertex.AreCollinear - Error: The input list of vertices is not a valid list. Returning None.")
|
75
|
-
return None
|
76
|
-
vertexList = [x for x in vertices if isinstance(x, topologic.Vertex)]
|
77
|
-
if len(vertexList) < 2:
|
78
|
-
print("Vertex.AreCollinear - Error: The input list of vertices does not contain sufficient valid vertices. Returning None.")
|
79
|
-
return None
|
80
|
-
if len(vertexList) < 3:
|
81
|
-
return True # Any two vertices can form a line!
|
82
|
-
cluster = Topology.SelfMerge(Cluster.ByTopologies(vertexList), tolerance=tolerance)
|
83
|
-
vertexList = Topology.Vertices(cluster)
|
84
|
-
slices = []
|
85
|
-
for i in range(2,len(vertexList)):
|
86
|
-
slices.append([vertexList[0], vertexList[1], vertexList[i]])
|
87
|
-
for slice in slices:
|
88
|
-
if not areCollinear(slice, tolerance=tolerance):
|
89
|
-
return False
|
90
|
-
return True
|
91
|
-
|
92
|
-
@staticmethod
|
93
|
-
def AreIpsilateral(vertices: list, face: topologic.Face) -> bool:
|
94
|
-
"""
|
95
|
-
Returns True if the input list of vertices are on one side of a face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
96
|
-
|
97
|
-
Parameters
|
98
|
-
----------
|
99
|
-
vertices : list
|
100
|
-
The input list of vertices.
|
101
|
-
face : topologic.Face
|
102
|
-
The input face
|
103
|
-
|
104
|
-
Returns
|
105
|
-
-------
|
106
|
-
bool
|
107
|
-
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
108
|
-
|
109
|
-
"""
|
110
|
-
def check(dot_productA, pointB, pointC, normal):
|
111
|
-
# Calculate the dot products of the vectors from the surface point to each of the input points.
|
112
|
-
dot_productB = (pointB[0] - pointC[0]) * normal[0] + \
|
113
|
-
(pointB[1] - pointC[1]) * normal[1] + \
|
114
|
-
(pointB[2] - pointC[2]) * normal[2]
|
115
|
-
|
116
|
-
# Check if both points are on the same side of the surface.
|
117
|
-
if dot_productA * dot_productB > 0:
|
118
|
-
return True
|
119
|
-
|
120
|
-
# Check if both points are on opposite sides of the surface.
|
121
|
-
elif dot_productA * dot_productB < 0:
|
122
|
-
return False
|
123
|
-
|
124
|
-
# Otherwise, at least one point is on the surface.
|
125
|
-
else:
|
126
|
-
return True
|
127
|
-
|
128
|
-
from topologicpy.Vertex import Vertex
|
129
|
-
from topologicpy.Face import Face
|
130
|
-
|
131
|
-
if not isinstance(face, topologic.Face):
|
132
|
-
return None
|
133
|
-
vertexList = [x for x in vertices if isinstance(x, topologic.Vertex)]
|
134
|
-
if len(vertexList) < 2:
|
135
|
-
return None
|
136
|
-
pointA = Vertex.Coordinates(vertexList[0])
|
137
|
-
pointC = Vertex.Coordinates(Face.VertexByParameters(face, 0.5, 0.5))
|
138
|
-
normal = Face.Normal(face)
|
139
|
-
dot_productA = (pointA[0] - pointC[0]) * normal[0] + \
|
140
|
-
(pointA[1] - pointC[1]) * normal[1] + \
|
141
|
-
(pointA[2] - pointC[2]) * normal[2]
|
142
|
-
for i in range(1, len(vertexList)):
|
143
|
-
pointB = Vertex.Coordinates(vertexList[i])
|
144
|
-
if not check(dot_productA, pointB, pointC, normal):
|
145
|
-
return False
|
146
|
-
return True
|
147
|
-
|
148
|
-
@staticmethod
|
149
|
-
def AreIpsilateralCluster(cluster: topologic.Cluster, face: topologic.Face) -> bool:
|
150
|
-
"""
|
151
|
-
Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
152
|
-
|
153
|
-
Parameters
|
154
|
-
----------
|
155
|
-
cluster : topologic.Cluster
|
156
|
-
The input list of vertices.
|
157
|
-
face : topologic.Face
|
158
|
-
The input face
|
159
|
-
|
160
|
-
Returns
|
161
|
-
-------
|
162
|
-
bool
|
163
|
-
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
164
|
-
|
165
|
-
"""
|
166
|
-
from topologicpy.Topology import Topology
|
167
|
-
if not isinstance(cluster, topologic.Topology):
|
168
|
-
return None
|
169
|
-
vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
|
170
|
-
return Vertex.AreIpsilateral(vertices, face)
|
171
|
-
|
172
|
-
@staticmethod
|
173
|
-
def AreOnSameSide(vertices: list, face: topologicpy.Face.Face) -> bool:
|
174
|
-
"""
|
175
|
-
Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
176
|
-
|
177
|
-
Parameters
|
178
|
-
----------
|
179
|
-
vertices : list
|
180
|
-
The input list of vertices.
|
181
|
-
face : topologic.Face
|
182
|
-
The input face
|
183
|
-
|
184
|
-
Returns
|
185
|
-
-------
|
186
|
-
bool
|
187
|
-
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
188
|
-
|
189
|
-
"""
|
190
|
-
return Vertex.AreIpsilateral(vertices, face)
|
191
|
-
|
192
|
-
@staticmethod
|
193
|
-
def AreOnSameSideCluster(cluster: topologic.Cluster, face: topologic.Face) -> bool:
|
194
|
-
"""
|
195
|
-
Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
196
|
-
|
197
|
-
Parameters
|
198
|
-
----------
|
199
|
-
cluster : topologic.Cluster
|
200
|
-
The input list of vertices.
|
201
|
-
face : topologic.Face
|
202
|
-
The input face
|
203
|
-
|
204
|
-
Returns
|
205
|
-
-------
|
206
|
-
bool
|
207
|
-
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
208
|
-
|
209
|
-
"""
|
210
|
-
from topologicpy.Topology import Topology
|
211
|
-
if not isinstance(cluster, topologic.Topology):
|
212
|
-
return None
|
213
|
-
vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
|
214
|
-
return Vertex.AreIpsilateral(vertices, face)
|
215
|
-
|
216
|
-
@staticmethod
|
217
|
-
def ByCoordinates(*args, **kwargs) -> topologic.Vertex:
|
218
|
-
"""
|
219
|
-
Creates a vertex at the coordinates specified by the x, y, z inputs. You can call this method using a list of coordinates or individually.
|
220
|
-
Examples:
|
221
|
-
v = Vertex.ByCoordinates(3.4, 5.7, 2.8)
|
222
|
-
v = Vertex.ByCoordinates([3.4, 5.7, 2.8])
|
223
|
-
v = Vertex.ByCoordinates(x=3.4, y=5.7, z=2.8)
|
224
|
-
|
225
|
-
Parameters
|
226
|
-
----------
|
227
|
-
x : float , optional
|
228
|
-
The X coordinate. The default is 0.
|
229
|
-
y : float , optional
|
230
|
-
The Y coordinate. The default is 0.
|
231
|
-
z : float , optional
|
232
|
-
The Z coordinate. The defaults is 0.
|
233
|
-
|
234
|
-
Returns
|
235
|
-
-------
|
236
|
-
topologic.Vertex
|
237
|
-
The created vertex.
|
238
|
-
|
239
|
-
"""
|
240
|
-
import numbers
|
241
|
-
x = None
|
242
|
-
y = None
|
243
|
-
z = None
|
244
|
-
if len(args) > 3 or len(kwargs.items()) > 3:
|
245
|
-
print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
|
246
|
-
return None
|
247
|
-
if len(args) > 0:
|
248
|
-
value = args[0]
|
249
|
-
if isinstance(value, list) and len(value) > 3:
|
250
|
-
print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
|
251
|
-
return None
|
252
|
-
elif isinstance(value, list) and len(value) == 3:
|
253
|
-
x = value[0]
|
254
|
-
y = value[1]
|
255
|
-
z = value[2]
|
256
|
-
elif isinstance(value, list) and len(value) == 2:
|
257
|
-
x = value[0]
|
258
|
-
y = value[1]
|
259
|
-
elif isinstance(value, list) and len(value) == 1:
|
260
|
-
x = value[0]
|
261
|
-
elif len(args) == 3:
|
262
|
-
x = args[0]
|
263
|
-
y = args[1]
|
264
|
-
z = args[2]
|
265
|
-
elif len(args) == 2:
|
266
|
-
x = args[0]
|
267
|
-
y = args[1]
|
268
|
-
elif len(args) == 1:
|
269
|
-
x = args[0]
|
270
|
-
for key, value in kwargs.items():
|
271
|
-
if "x" in key.lower():
|
272
|
-
if not x == None:
|
273
|
-
print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
|
274
|
-
return None
|
275
|
-
x = value
|
276
|
-
elif "y" in key.lower():
|
277
|
-
if not y == None:
|
278
|
-
print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
|
279
|
-
return None
|
280
|
-
y = value
|
281
|
-
elif "z" in key.lower():
|
282
|
-
if not z == None:
|
283
|
-
print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
|
284
|
-
return None
|
285
|
-
z = value
|
286
|
-
if x == None:
|
287
|
-
x = 0
|
288
|
-
if y == None:
|
289
|
-
y = 0
|
290
|
-
if z == None:
|
291
|
-
z = 0
|
292
|
-
if not isinstance(x, numbers.Number):
|
293
|
-
print("Vertex.ByCoordinates - Error: The x value is not a valid number. Returning None.")
|
294
|
-
return None
|
295
|
-
if not isinstance(y, numbers.Number):
|
296
|
-
print("Vertex.ByCoordinates - Error: The y value is not a valid number. Returning None.")
|
297
|
-
return None
|
298
|
-
if not isinstance(z, numbers.Number):
|
299
|
-
print("Vertex.ByCoordinates - Error: The z value is not a valid number. Returning None.")
|
300
|
-
return None
|
301
|
-
|
302
|
-
vertex = None
|
303
|
-
try:
|
304
|
-
vertex = topologic.Vertex.ByCoordinates(x, y, z)
|
305
|
-
except:
|
306
|
-
vertex = None
|
307
|
-
print("Vertex.ByCoordinates - Error: Could not create a topologic vertex. Returning None.")
|
308
|
-
return vertex
|
309
|
-
|
310
|
-
@staticmethod
|
311
|
-
def Centroid(vertices):
|
312
|
-
"""
|
313
|
-
Returns the centroid of the input list of vertices.
|
314
|
-
|
315
|
-
Parameters
|
316
|
-
-----------
|
317
|
-
vertices : list
|
318
|
-
The input list of vertices
|
319
|
-
|
320
|
-
Return
|
321
|
-
----------
|
322
|
-
topologic.Vertex
|
323
|
-
The computed centroid of the input list of vertices
|
324
|
-
"""
|
325
|
-
|
326
|
-
if not isinstance(vertices, list):
|
327
|
-
print("Vertex.Centroid - Error: The input vertices parameter is not a valid list. Returning None.")
|
328
|
-
return None
|
329
|
-
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
330
|
-
if len(vertices) < 1:
|
331
|
-
print("Vertex.Centroid - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
|
332
|
-
return None
|
333
|
-
if len(vertices) == 1:
|
334
|
-
return vertices[0]
|
335
|
-
cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
|
336
|
-
cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)
|
337
|
-
cz = sum(Vertex.Z(v) for v in vertices) / len(vertices)
|
338
|
-
return Vertex.ByCoordinates(cx, cy, cz)
|
339
|
-
|
340
|
-
@staticmethod
|
341
|
-
def Clockwise2D(vertices):
|
342
|
-
"""
|
343
|
-
Sorts the input list of vertices in a clockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.
|
344
|
-
|
345
|
-
Parameters
|
346
|
-
-----------
|
347
|
-
vertices : list
|
348
|
-
The input list of vertices
|
349
|
-
|
350
|
-
Return
|
351
|
-
-----------
|
352
|
-
list
|
353
|
-
The input list of vertices sorted in a counter clockwise fashion
|
354
|
-
|
355
|
-
"""
|
356
|
-
return list(reversed(Vertex.CounterClockwise2D(vertices)))
|
357
|
-
|
358
|
-
@staticmethod
|
359
|
-
def Coordinates(vertex: topologic.Vertex, outputType: str = "xyz", mantissa: int = 6) -> list:
|
360
|
-
"""
|
361
|
-
Returns the coordinates of the input vertex.
|
362
|
-
|
363
|
-
Parameters
|
364
|
-
----------
|
365
|
-
vertex : topologic.Vertex
|
366
|
-
The input vertex.
|
367
|
-
outputType : string, optional
|
368
|
-
The desired output type. Could be any permutation or substring of "xyz" or the string "matrix". The default is "xyz". The input is case insensitive and the coordinates will be returned in the specified order.
|
369
|
-
mantissa : int , optional
|
370
|
-
The desired length of the mantissa. The default is 6.
|
371
|
-
|
372
|
-
Returns
|
373
|
-
-------
|
374
|
-
list
|
375
|
-
The coordinates of the input vertex.
|
376
|
-
|
377
|
-
"""
|
378
|
-
if not isinstance(vertex, topologic.Vertex):
|
379
|
-
return None
|
380
|
-
x = round(vertex.X(), mantissa)
|
381
|
-
y = round(vertex.Y(), mantissa)
|
382
|
-
z = round(vertex.Z(), mantissa)
|
383
|
-
matrix = [[1, 0, 0, x],
|
384
|
-
[0, 1, 0, y],
|
385
|
-
[0, 0, 1, z],
|
386
|
-
[0, 0, 0, 1]]
|
387
|
-
output = []
|
388
|
-
outputType = outputType.lower()
|
389
|
-
if outputType == "matrix":
|
390
|
-
return matrix
|
391
|
-
else:
|
392
|
-
outputType = list(outputType)
|
393
|
-
for axis in outputType:
|
394
|
-
if axis == "x":
|
395
|
-
output.append(x)
|
396
|
-
elif axis == "y":
|
397
|
-
output.append(y)
|
398
|
-
elif axis == "z":
|
399
|
-
output.append(z)
|
400
|
-
return output
|
401
|
-
|
402
|
-
@staticmethod
|
403
|
-
def CounterClockwise2D(vertices):
|
404
|
-
"""
|
405
|
-
Sorts the input list of vertices in a counterclockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.
|
406
|
-
|
407
|
-
Parameters
|
408
|
-
-----------
|
409
|
-
vertices : list
|
410
|
-
The input list of vertices
|
411
|
-
|
412
|
-
Return
|
413
|
-
-----------
|
414
|
-
list
|
415
|
-
The input list of vertices sorted in a counter clockwise fashion
|
416
|
-
|
417
|
-
"""
|
418
|
-
import math
|
419
|
-
# find the centroid of the points
|
420
|
-
cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
|
421
|
-
cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)
|
422
|
-
|
423
|
-
# sort the points based on their angle with respect to the centroid
|
424
|
-
vertices.sort(key=lambda v: (math.atan2(Vertex.Y(v) - cy, Vertex.X(v) - cx) + 2 * math.pi) % (2 * math.pi))
|
425
|
-
return vertices
|
426
|
-
|
427
|
-
@staticmethod
|
428
|
-
def Degree(vertex: topologic.Vertex, hostTopology: topologic.Topology, topologyType: str = "edge"):
|
429
|
-
"""
|
430
|
-
Returns the vertex degree (the number of super topologies connected to it). See https://en.wikipedia.org/wiki/Degree_(graph_theory).
|
431
|
-
|
432
|
-
Parameters
|
433
|
-
----------
|
434
|
-
vertex : topologic.Vertex
|
435
|
-
The input vertex.
|
436
|
-
hostTopology : topologic.Topology
|
437
|
-
The input host topology in which to search for the connected super topologies.
|
438
|
-
topologyType : str , optional
|
439
|
-
The topology type to search for. This can be any of "edge", "wire", "face", "shell", "cell", "cellcomplex", "cluster". It is case insensitive. If set to None, the immediate supertopology type is searched for. The default is None.
|
440
|
-
|
441
|
-
Returns
|
442
|
-
-------
|
443
|
-
int
|
444
|
-
The number of super topologies connected to this vertex
|
445
|
-
|
446
|
-
"""
|
447
|
-
from topologicpy.Topology import Topology
|
448
|
-
|
449
|
-
if not isinstance(vertex, topologic.Vertex):
|
450
|
-
print("Vertex.Degree - Error: The input vertex parameter is not a valid topologic vertex. Returning None.")
|
451
|
-
if not isinstance(hostTopology, topologic.Topology):
|
452
|
-
print("Vertex.Degree - Error: The input hostTopology parameter is not a valid topologic topology. Returning None.")
|
453
|
-
superTopologies = Topology.SuperTopologies(topology=vertex, hostTopology=hostTopology, topologyType=topologyType)
|
454
|
-
return len(superTopologies)
|
455
|
-
|
456
|
-
|
457
|
-
@staticmethod
|
458
|
-
def Distance(vertex: topologic.Vertex, topology: topologic.Topology, includeCentroid: bool =True,
|
459
|
-
mantissa: int = 6) -> float:
|
460
|
-
"""
|
461
|
-
Returns the distance between the input vertex and the input topology. This method returns the distance to the closest sub-topology in the input topology, optionally including its centroid.
|
462
|
-
|
463
|
-
Parameters
|
464
|
-
----------
|
465
|
-
vertex : topologic.Vertex
|
466
|
-
The input vertex.
|
467
|
-
topology : topologic.Topology
|
468
|
-
The input topology.
|
469
|
-
includeCentroid : bool
|
470
|
-
If set to True, the centroid of the input topology will be considered in finding the nearest subTopology to the input vertex. The default is True.
|
471
|
-
mantissa : int , optional
|
472
|
-
The desired length of the mantissa. The default is 6.
|
473
|
-
|
474
|
-
Returns
|
475
|
-
-------
|
476
|
-
float
|
477
|
-
The distance between the input vertex and the input topology.
|
478
|
-
|
479
|
-
"""
|
480
|
-
from topologicpy.Edge import Edge
|
481
|
-
from topologicpy.Face import Face
|
482
|
-
import math
|
483
|
-
|
484
|
-
def distance_point_to_point(point1, point2):
|
485
|
-
# Convert input points to NumPy arrays
|
486
|
-
point1 = np.array(point1)
|
487
|
-
point2 = np.array(point2)
|
488
|
-
|
489
|
-
# Calculate the Euclidean distance
|
490
|
-
distance = np.linalg.norm(point1 - point2)
|
491
|
-
|
492
|
-
return distance
|
493
|
-
|
494
|
-
def distance_point_to_line(point, line_start, line_end):
|
495
|
-
# Convert input points to NumPy arrays for vector operations
|
496
|
-
point = np.array(point)
|
497
|
-
line_start = np.array(line_start)
|
498
|
-
line_end = np.array(line_end)
|
499
|
-
|
500
|
-
# Calculate the direction vector of the edge
|
501
|
-
line_direction = line_end - line_start
|
502
|
-
|
503
|
-
# Vector from the edge's starting point to the point
|
504
|
-
point_to_start = point - line_start
|
505
|
-
|
506
|
-
# Calculate the parameter 't' where the projection of the point onto the edge occurs
|
507
|
-
if np.dot(line_direction, line_direction) == 0:
|
508
|
-
t = 0
|
509
|
-
else:
|
510
|
-
t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
|
511
|
-
|
512
|
-
# Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
|
513
|
-
if t < 0:
|
514
|
-
return np.linalg.norm(point - line_start)
|
515
|
-
elif t > 1:
|
516
|
-
return np.linalg.norm(point - line_end)
|
517
|
-
|
518
|
-
# Calculate the closest point on the edge to the given point
|
519
|
-
closest_point = line_start + t * line_direction
|
520
|
-
|
521
|
-
# Calculate the distance between the closest point and the given point
|
522
|
-
distance = np.linalg.norm(point - closest_point)
|
523
|
-
|
524
|
-
return distance
|
525
|
-
|
526
|
-
def distance_to_vertex(vertexA, vertexB):
|
527
|
-
a = (Vertex.X(vertexA), Vertex.Y(vertexA), Vertex.Z(vertexA))
|
528
|
-
b = (Vertex.X(vertexB), Vertex.Y(vertexB), Vertex.Z(vertexB))
|
529
|
-
return distance_point_to_point(a, b)
|
530
|
-
|
531
|
-
def distance_to_edge(vertex, edge):
|
532
|
-
a = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
|
533
|
-
sv = Edge.StartVertex(edge)
|
534
|
-
ev = Edge.EndVertex(edge)
|
535
|
-
svp = (Vertex.X(sv), Vertex.Y(sv), Vertex.Z(sv))
|
536
|
-
evp = (Vertex.X(ev), Vertex.Y(ev), Vertex.Z(ev))
|
537
|
-
return distance_point_to_line(a,svp, evp)
|
538
|
-
|
539
|
-
def distance_to_face(vertex, face, includeCentroid):
|
540
|
-
v_proj = Vertex.Project(vertex, face, mantissa=mantissa)
|
541
|
-
if not Vertex.IsInternal(v_proj, face):
|
542
|
-
vertices = Topology.Vertices(topology)
|
543
|
-
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
544
|
-
edges = Topology.Edges(topology)
|
545
|
-
distances += [distance_to_edge(vertex, e) for e in edges]
|
546
|
-
if includeCentroid:
|
547
|
-
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
548
|
-
return min(distances)
|
549
|
-
dic = Face.PlaneEquation(face)
|
550
|
-
a = dic["a"]
|
551
|
-
b = dic["b"]
|
552
|
-
c = dic["c"]
|
553
|
-
d = dic["d"]
|
554
|
-
x1, y1, z1 = Vertex.Coordinates(vertex)
|
555
|
-
d = abs((a * x1 + b * y1 + c * z1 + d))
|
556
|
-
e = (math.sqrt(a * a + b * b + c * c))
|
557
|
-
if e == 0:
|
558
|
-
return 0
|
559
|
-
return d/e
|
560
|
-
if not isinstance(vertex, topologic.Vertex) or not isinstance(topology, topologic.Topology):
|
561
|
-
return None
|
562
|
-
if isinstance(topology, topologic.Vertex):
|
563
|
-
return round(distance_to_vertex(vertex,topology), mantissa)
|
564
|
-
elif isinstance(topology, topologic.Edge):
|
565
|
-
return round(distance_to_edge(vertex,topology), mantissa)
|
566
|
-
elif isinstance(topology, topologic.Wire):
|
567
|
-
vertices = Topology.Vertices(topology)
|
568
|
-
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
569
|
-
edges = Topology.Edges(topology)
|
570
|
-
distances += [distance_to_edge(vertex, e) for e in edges]
|
571
|
-
if includeCentroid:
|
572
|
-
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
573
|
-
return round(min(distances), mantissa)
|
574
|
-
elif isinstance(topology, topologic.Face):
|
575
|
-
vertices = Topology.Vertices(topology)
|
576
|
-
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
577
|
-
edges = Topology.Edges(topology)
|
578
|
-
distances += [distance_to_edge(vertex, e) for e in edges]
|
579
|
-
distances.append(distance_to_face(vertex,topology, includeCentroid))
|
580
|
-
if includeCentroid:
|
581
|
-
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
582
|
-
return round(min(distances), mantissa)
|
583
|
-
elif isinstance(topology, topologic.Shell) or isinstance(topology, topologic.Cell) or isinstance(topology, topologic.CellComplex) or isinstance(topology, topologic.Cluster):
|
584
|
-
vertices = Topology.Vertices(topology)
|
585
|
-
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
586
|
-
edges = Topology.Edges(topology)
|
587
|
-
distances += [distance_to_edge(vertex, e) for e in edges]
|
588
|
-
faces = Topology.Faces(topology)
|
589
|
-
distances += [distance_to_face(vertex, f, includeCentroid) for f in faces]
|
590
|
-
if includeCentroid:
|
591
|
-
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
592
|
-
return round(min(distances), mantissa)
|
593
|
-
else:
|
594
|
-
print("Vertex.Distance - Error: Could not recognize the input topology. Returning None.")
|
595
|
-
return None
|
596
|
-
|
597
|
-
@staticmethod
|
598
|
-
def EnclosingCell(vertex: topologic.Vertex, topology: topologic.Topology, exclusive: bool = True, tolerance: float = 0.0001) -> list:
|
599
|
-
"""
|
600
|
-
Returns the list of Cells found in the input topology that enclose the input vertex.
|
601
|
-
|
602
|
-
Parameters
|
603
|
-
----------
|
604
|
-
vertex : topologic.Vertex
|
605
|
-
The input vertex.
|
606
|
-
topology : topologic.Topology
|
607
|
-
The input topology.
|
608
|
-
exclusive : bool , optional
|
609
|
-
If set to True, return only the first found enclosing cell. The default is True.
|
610
|
-
tolerance : float , optional
|
611
|
-
The tolerance for computing if the input vertex is enclosed in a cell. The default is 0.0001.
|
612
|
-
|
613
|
-
Returns
|
614
|
-
-------
|
615
|
-
list
|
616
|
-
The list of enclosing cells.
|
617
|
-
|
618
|
-
"""
|
619
|
-
|
620
|
-
def boundingBox(cell):
|
621
|
-
vertices = []
|
622
|
-
_ = cell.Vertices(None, vertices)
|
623
|
-
x = []
|
624
|
-
y = []
|
625
|
-
z = []
|
626
|
-
for aVertex in vertices:
|
627
|
-
x.append(aVertex.X())
|
628
|
-
y.append(aVertex.Y())
|
629
|
-
z.append(aVertex.Z())
|
630
|
-
return ([min(x), min(y), min(z), max(x), max(y), max(z)])
|
631
|
-
|
632
|
-
if isinstance(topology, topologic.Cell):
|
633
|
-
cells = [topology]
|
634
|
-
elif isinstance(topology, topologic.Cluster) or isinstance(topology, topologic.CellComplex):
|
635
|
-
cells = []
|
636
|
-
_ = topology.Cells(None, cells)
|
637
|
-
else:
|
638
|
-
return None
|
639
|
-
if len(cells) < 1:
|
640
|
-
return None
|
641
|
-
enclosingCells = []
|
642
|
-
for i in range(len(cells)):
|
643
|
-
bbox = boundingBox(cells[i])
|
644
|
-
if ((vertex.X() < bbox[0]) or (vertex.Y() < bbox[1]) or (vertex.Z() < bbox[2]) or (vertex.X() > bbox[3]) or (vertex.Y() > bbox[4]) or (vertex.Z() > bbox[5])) == False:
|
645
|
-
if topologic.CellUtility.Contains(cells[i], vertex, tolerance) == 0:
|
646
|
-
if exclusive:
|
647
|
-
return([cells[i]])
|
648
|
-
else:
|
649
|
-
enclosingCells.append(cells[i])
|
650
|
-
return enclosingCells
|
651
|
-
|
652
|
-
@staticmethod
|
653
|
-
def Fuse(vertices: list, mantissa: int = 6, tolerance: float = 0.0001):
|
654
|
-
"""
|
655
|
-
Returns a list of vertices where vertices within a specified tolerance distance are fused while retaining duplicates, ensuring that vertices with nearly identical coordinates are replaced by a single shared coordinate.
|
656
|
-
|
657
|
-
Parameters
|
658
|
-
----------
|
659
|
-
vertices : list
|
660
|
-
The input list of topologic vertices.
|
661
|
-
mantissa : int , optional
|
662
|
-
The desired length of the mantissa for retrieving vertex coordinates. The default is 6.
|
663
|
-
tolerance : float , optional
|
664
|
-
The desired tolerance for computing if vertices need to be fused. Any vertices that are closer to each other than this tolerance will be fused. The default is 0.0001.
|
665
|
-
|
666
|
-
Returns
|
667
|
-
-------
|
668
|
-
list
|
669
|
-
The list of fused vertices. This list contains the same number of vertices and in the same order as the input list of vertices. However, the coordinates
|
670
|
-
of these vertices have now been modified so that they are exactly the same with other vertices that are within the tolerance distance.
|
671
|
-
"""
|
672
|
-
|
673
|
-
import numpy as np
|
674
|
-
|
675
|
-
def fuse_vertices(vertices, tolerance):
|
676
|
-
fused_vertices = []
|
677
|
-
merged_indices = {}
|
678
|
-
|
679
|
-
for idx, vertex in enumerate(vertices):
|
680
|
-
if idx in merged_indices:
|
681
|
-
fused_vertices.append(fused_vertices[merged_indices[idx]])
|
682
|
-
continue
|
683
|
-
|
684
|
-
merged_indices[idx] = len(fused_vertices)
|
685
|
-
fused_vertex = vertex
|
686
|
-
for i in range(idx + 1, len(vertices)):
|
687
|
-
if i in merged_indices:
|
688
|
-
continue
|
689
|
-
|
690
|
-
other_vertex = vertices[i]
|
691
|
-
distance = np.linalg.norm(np.array(vertex) - np.array(other_vertex))
|
692
|
-
if distance < tolerance:
|
693
|
-
# Choose the coordinate with the least amount of decimal points
|
694
|
-
if count_decimal_points(other_vertex) < count_decimal_points(fused_vertex):
|
695
|
-
fused_vertex = other_vertex
|
696
|
-
|
697
|
-
merged_indices[i] = len(fused_vertices)
|
698
|
-
|
699
|
-
fused_vertices.append(fused_vertex)
|
700
|
-
|
701
|
-
return fused_vertices
|
702
|
-
def count_decimal_points(vertex):
|
703
|
-
# Count the number of decimal points in the coordinates
|
704
|
-
decimals_list = []
|
705
|
-
for coord in vertex:
|
706
|
-
coord_str = str(coord)
|
707
|
-
if '.' in coord_str:
|
708
|
-
decimals_list.append(len(coord_str.split('.')[1]))
|
709
|
-
elif 'e' in coord_str:
|
710
|
-
decimals_list.append(int(coord_str.split('e')[1].replace('-','')))
|
711
|
-
return max(decimals_list)
|
712
|
-
|
713
|
-
|
714
|
-
if not isinstance(vertices, list):
|
715
|
-
print("Vertex.Fuse - Error: The input vertices parameter is not a valid list. Returning None.")
|
716
|
-
return None
|
717
|
-
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
718
|
-
if len(vertices) == 0:
|
719
|
-
print("Vertex.Fuse - Error: The input vertices parameter does not contain any valid topologic vertices. Returning None.")
|
720
|
-
return None
|
721
|
-
|
722
|
-
vertices = [(Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa), Vertex.Z(v, mantissa=mantissa)) for v in vertices]
|
723
|
-
fused_vertices = fuse_vertices(vertices, tolerance)
|
724
|
-
return_vertices = [Vertex.ByCoordinates(list(coord)) for coord in fused_vertices]
|
725
|
-
return return_vertices
|
726
|
-
|
727
|
-
@staticmethod
|
728
|
-
def Index(vertex: topologic.Vertex, vertices: list, strict: bool = False, tolerance: float = 0.0001) -> int:
|
729
|
-
"""
|
730
|
-
Returns index of the input vertex in the input list of vertices
|
731
|
-
|
732
|
-
Parameters
|
733
|
-
----------
|
734
|
-
vertex : topologic.Vertex
|
735
|
-
The input vertex.
|
736
|
-
vertices : list
|
737
|
-
The input list of vertices.
|
738
|
-
strict : bool , optional
|
739
|
-
If set to True, the vertex must be strictly identical to the one found in the list. Otherwise, a distance comparison is used. The default is False.
|
740
|
-
tolerance : float , optional
|
741
|
-
The tolerance for computing if the input vertex is identical to a vertex from the list. The default is 0.0001.
|
742
|
-
|
743
|
-
Returns
|
744
|
-
-------
|
745
|
-
int
|
746
|
-
The index of the input vertex in the input list of vertices.
|
747
|
-
|
748
|
-
"""
|
749
|
-
from topologicpy.Topology import Topology
|
750
|
-
if not isinstance(vertex, topologic.Vertex):
|
751
|
-
return None
|
752
|
-
if not isinstance(vertices, list):
|
753
|
-
return None
|
754
|
-
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
755
|
-
if len(vertices) == 0:
|
756
|
-
return None
|
757
|
-
for i in range(len(vertices)):
|
758
|
-
if strict:
|
759
|
-
if Topology.IsSame(vertex, vertices[i]):
|
760
|
-
return i
|
761
|
-
else:
|
762
|
-
d = Vertex.Distance(vertex, vertices[i])
|
763
|
-
if d < tolerance:
|
764
|
-
return i
|
765
|
-
return None
|
766
|
-
|
767
|
-
@staticmethod
|
768
|
-
def InterpolateValue(vertex, vertices, n=3, key="intensity", tolerance=0.0001):
|
769
|
-
"""
|
770
|
-
Interpolates the value of the input vertex based on the values of the *n* nearest vertices.
|
771
|
-
|
772
|
-
Parameters
|
773
|
-
----------
|
774
|
-
vertex : topologic.Vertex
|
775
|
-
The input vertex.
|
776
|
-
vertices : list
|
777
|
-
The input list of vertices.
|
778
|
-
n : int , optional
|
779
|
-
The maximum number of nearest vertices to consider. The default is 3.
|
780
|
-
key : str , optional
|
781
|
-
The key that holds the value to be interpolated in the dictionaries of the vertices. The default is "intensity".
|
782
|
-
tolerance : float , optional
|
783
|
-
The tolerance for computing if the input vertex is coincident with another vertex in the input list of vertices. The default is 0.0001.
|
784
|
-
|
785
|
-
Returns
|
786
|
-
-------
|
787
|
-
topologic.vertex
|
788
|
-
The input vertex with the interpolated value stored in its dictionary at the key specified by the input key. Other keys and values in the dictionary are preserved.
|
789
|
-
|
790
|
-
"""
|
791
|
-
|
792
|
-
def interpolate_value(point, data_points, n, tolerance=0.0001):
|
793
|
-
"""
|
794
|
-
Interpolates the value associated with a point in 3D by averaging the values of the n nearest points.
|
795
|
-
The influence of the adjacent points is inversely proportional to their distance from the input point.
|
796
|
-
|
797
|
-
Args:
|
798
|
-
data_points (list): A list of tuples, each representing a data point in 3D space as (x, y, z, value).
|
799
|
-
The 'value' represents the value associated with that data point.
|
800
|
-
point (tuple): A tuple representing the point in 3D space as (x, y, z) for which we want to interpolate a value.
|
801
|
-
n (int): The number of nearest points to consider for interpolation.
|
802
|
-
|
803
|
-
Returns:
|
804
|
-
The interpolated value for the input point.
|
805
|
-
"""
|
806
|
-
# Calculate the distances between the input point and all data points
|
807
|
-
distances = [(distance(p[:3], point), p[3]) for p in data_points]
|
808
|
-
|
809
|
-
# Sort the distances in ascending order
|
810
|
-
sorted_distances = sorted(distances, key=lambda x: x[0])
|
811
|
-
|
812
|
-
# Take the n nearest points
|
813
|
-
nearest_points = sorted_distances[:n]
|
814
|
-
|
815
|
-
n_p = nearest_points[0]
|
816
|
-
n_d = n_p[0]
|
817
|
-
if n_d < tolerance:
|
818
|
-
return n_p[1]
|
819
|
-
|
820
|
-
# Calculate the weights for each nearest point based on inverse distance
|
821
|
-
|
822
|
-
weights = [(1/d[0], d[1]) for d in nearest_points]
|
823
|
-
|
824
|
-
# Normalize the weights so they sum to 1
|
825
|
-
total_weight = sum(w[0] for w in weights)
|
826
|
-
normalized_weights = [(w[0]/total_weight, w[1]) for w in weights]
|
827
|
-
|
828
|
-
# Interpolate the value as the weighted average of the nearest points
|
829
|
-
interpolated_value = sum(w[0]*w[1] for w in normalized_weights)
|
830
|
-
|
831
|
-
return interpolated_value
|
832
|
-
|
833
|
-
def distance(point1, point2):
|
834
|
-
"""
|
835
|
-
Calculates the Euclidean distance between two points in 3D space.
|
836
|
-
|
837
|
-
Args:
|
838
|
-
point1 (tuple): A tuple representing a point in 3D space as (x, y, z).
|
839
|
-
point2 (tuple): A tuple representing a point in 3D space as (x, y, z).
|
840
|
-
|
841
|
-
Returns:
|
842
|
-
The Euclidean distance between the two points.
|
843
|
-
"""
|
844
|
-
return ((point1[0]-point2[0])**2 + (point1[1]-point2[1])**2 + (point1[2]-point2[2])**2)**0.5
|
845
|
-
|
846
|
-
from topologicpy.Topology import Topology
|
847
|
-
from topologicpy.Dictionary import Dictionary
|
848
|
-
|
849
|
-
if not isinstance(vertex, topologic.Vertex):
|
850
|
-
return None
|
851
|
-
if not isinstance(vertices, list):
|
852
|
-
return None
|
853
|
-
|
854
|
-
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
855
|
-
if len(vertices) == 0:
|
856
|
-
return None
|
857
|
-
|
858
|
-
point = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
|
859
|
-
data_points = []
|
860
|
-
for v in vertices:
|
861
|
-
d = Topology.Dictionary(v)
|
862
|
-
value = Dictionary.ValueAtKey(d, key)
|
863
|
-
if not value == None:
|
864
|
-
if type(value) == int or type(value) == float:
|
865
|
-
data_points.append((Vertex.X(v), Vertex.Y(v), Vertex.Z(v), value))
|
866
|
-
if len(data_points) == 0:
|
867
|
-
return None
|
868
|
-
if n > len(data_points):
|
869
|
-
n = len(data_points)
|
870
|
-
value = interpolate_value(point, data_points, n, tolerance=0.0001)
|
871
|
-
d = Topology.Dictionary(vertex)
|
872
|
-
d = Dictionary.SetValueAtKey(d, key, value)
|
873
|
-
vertex = Topology.SetDictionary(vertex, d)
|
874
|
-
return vertex
|
875
|
-
|
876
|
-
@staticmethod
|
877
|
-
def IsCoincident(vertexA: topologic.Vertex, vertexB: topologic.Vertex, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
878
|
-
"""
|
879
|
-
Returns True if the input vertexA is coincident with the input vertexB. Returns False otherwise.
|
880
|
-
|
881
|
-
Parameters
|
882
|
-
----------
|
883
|
-
vertexA : topologic.Vertex
|
884
|
-
The first input vertex.
|
885
|
-
vertexB : topologic.Vertex
|
886
|
-
The second input vertex.
|
887
|
-
tolerance : float , optional
|
888
|
-
The tolerance for computing if the input vertexA is coincident with the input vertexB. The default is 0.0001.
|
889
|
-
|
890
|
-
Returns
|
891
|
-
-------
|
892
|
-
bool
|
893
|
-
True if the input vertexA is coincident with the input vertexB. False otherwise.
|
894
|
-
|
895
|
-
"""
|
896
|
-
if not isinstance(vertexA, topologic.Vertex):
|
897
|
-
if not silent:
|
898
|
-
print("Vertex.IsCoincident - Error: The input vertexA parameter is not a valid vertex. Returning None.")
|
899
|
-
return None
|
900
|
-
if not isinstance(vertexB, topologic.Vertex):
|
901
|
-
if not silent:
|
902
|
-
print("Vertex.IsICoincident - Error: The input vertexB parameter is not a valid vertex. Returning None.")
|
903
|
-
return None
|
904
|
-
return Vertex.IsInternal(vertexA, vertexB, tolerance=tolerance, silent=silent)
|
905
|
-
|
906
|
-
@staticmethod
|
907
|
-
def IsExternal(vertex: topologic.Vertex, topology: topologic.Topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
908
|
-
"""
|
909
|
-
Returns True if the input vertex is external to the input topology. Returns False otherwise.
|
910
|
-
|
911
|
-
Parameters
|
912
|
-
----------
|
913
|
-
vertex : topologic.Vertex
|
914
|
-
The input vertex.
|
915
|
-
topology : topologic.Topology
|
916
|
-
The input topology.
|
917
|
-
tolerance : float , optional
|
918
|
-
The tolerance for computing if the input vertex is external to the input topology. The default is 0.0001.
|
919
|
-
silent : bool , optional
|
920
|
-
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
|
921
|
-
|
922
|
-
Returns
|
923
|
-
-------
|
924
|
-
bool
|
925
|
-
True if the input vertex is external to the input topology. False otherwise.
|
926
|
-
|
927
|
-
"""
|
928
|
-
|
929
|
-
if not isinstance(vertex, topologic.Vertex):
|
930
|
-
if not silent:
|
931
|
-
print("Vertex.IsExternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
|
932
|
-
return None
|
933
|
-
if not isinstance(topology, topologic.Topology):
|
934
|
-
if not silent:
|
935
|
-
print("Vertex.IsExternal - Error: The input topology parameter is not a valid topology. Returning None.")
|
936
|
-
return None
|
937
|
-
return not (Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent) or Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent))
|
938
|
-
|
939
|
-
@staticmethod
|
940
|
-
def IsInternal(vertex: topologic.Vertex, topology: topologic.Topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
941
|
-
"""
|
942
|
-
Returns True if the input vertex is inside the input topology. Returns False otherwise.
|
943
|
-
|
944
|
-
Parameters
|
945
|
-
----------
|
946
|
-
vertex : topologic.Vertex
|
947
|
-
The input vertex.
|
948
|
-
topology : topologic.Topology
|
949
|
-
The input topology.
|
950
|
-
tolerance : float , optional
|
951
|
-
The tolerance for computing if the input vertex is internal to the input topology. The default is 0.0001.
|
952
|
-
silent : bool , optional
|
953
|
-
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
|
954
|
-
|
955
|
-
Returns
|
956
|
-
-------
|
957
|
-
bool
|
958
|
-
True if the input vertex is internal to the input topology. False otherwise.
|
959
|
-
|
960
|
-
"""
|
961
|
-
from topologicpy.Edge import Edge
|
962
|
-
from topologicpy.Wire import Wire
|
963
|
-
from topologicpy.Face import Face
|
964
|
-
from topologicpy.Shell import Shell
|
965
|
-
from topologicpy.CellComplex import CellComplex
|
966
|
-
from topologicpy.Cluster import Cluster
|
967
|
-
from topologicpy.Topology import Topology
|
968
|
-
if not isinstance(vertex, topologic.Vertex):
|
969
|
-
if not silent:
|
970
|
-
print("Vertex.IsInternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
|
971
|
-
return None
|
972
|
-
if not isinstance(topology, topologic.Topology):
|
973
|
-
if not silent:
|
974
|
-
print("Vertex.IsInternal - Error: The input topology parameter is not a valid topology. Returning None.")
|
975
|
-
return None
|
976
|
-
|
977
|
-
if isinstance(topology, topologic.Vertex):
|
978
|
-
return Vertex.Distance(vertex, topology) < tolerance
|
979
|
-
elif isinstance(topology, topologic.Edge):
|
980
|
-
try:
|
981
|
-
parameter = topologic.EdgeUtility.ParameterAtPoint(topology, vertex)
|
982
|
-
except:
|
983
|
-
parameter = 400 #aribtrary large number greater than 1
|
984
|
-
return 0 <= parameter <= 1
|
985
|
-
elif isinstance(topology, topologic.Wire):
|
986
|
-
vertices = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) > 1]
|
987
|
-
edges = Wire.Edges(topology)
|
988
|
-
sub_list = vertices + edges
|
989
|
-
for sub in sub_list:
|
990
|
-
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
991
|
-
return True
|
992
|
-
return False
|
993
|
-
elif isinstance(topology, topologic.Face):
|
994
|
-
# Test the distance first
|
995
|
-
if Vertex.PerpendicularDistance(vertex, topology) > tolerance:
|
996
|
-
return False
|
997
|
-
if Vertex.IsPeripheral(vertex, topology):
|
998
|
-
return False
|
999
|
-
normal = Face.Normal(topology)
|
1000
|
-
proj_v = Vertex.Project(vertex, topology)
|
1001
|
-
v1 = Topology.TranslateByDirectionDistance(proj_v, normal, 1)
|
1002
|
-
v2 = Topology.TranslateByDirectionDistance(proj_v, normal, -1)
|
1003
|
-
edge = Edge.ByVertices(v1, v2)
|
1004
|
-
intersect = edge.Intersect(topology)
|
1005
|
-
if intersect == None:
|
1006
|
-
return False
|
1007
|
-
return True
|
1008
|
-
elif isinstance(topology, topologic.Shell):
|
1009
|
-
if Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent):
|
1010
|
-
return False
|
1011
|
-
else:
|
1012
|
-
edges = Topology.Edges(topology)
|
1013
|
-
for edge in edges:
|
1014
|
-
if Vertex.IsInternal(vertex, edge, tolerance=tolerance, silent=silent):
|
1015
|
-
return True
|
1016
|
-
faces = Topology.Faces(topology)
|
1017
|
-
for face in faces:
|
1018
|
-
if Vertex.IsInternal(vertex, face, tolerance=tolerance, silent=silent):
|
1019
|
-
return True
|
1020
|
-
return False
|
1021
|
-
elif isinstance(topology, topologic.Cell):
|
1022
|
-
return topologic.CellUtility.Contains(topology, vertex, tolerance) == 0
|
1023
|
-
elif isinstance(topology, topologic.CellComplex):
|
1024
|
-
ext_boundary = CellComplex.ExternalBoundary(topology)
|
1025
|
-
return Vertex.IsInternal(vertex, ext_boundary, tolerance=tolerance, silent=silent)
|
1026
|
-
elif isinstance(topology, topologic.Cluster):
|
1027
|
-
sub_list = Cluster.FreeTopologies(topology)
|
1028
|
-
for sub in sub_list:
|
1029
|
-
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1030
|
-
return True
|
1031
|
-
return False
|
1032
|
-
return False
|
1033
|
-
|
1034
|
-
|
1035
|
-
@staticmethod
|
1036
|
-
def IsPeripheral(vertex: topologic.Vertex, topology: topologic.Topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
1037
|
-
"""
|
1038
|
-
Returns True if the input vertex is peripheral to the input topology. Returns False otherwise.
|
1039
|
-
A vertex is said to be peripheral to the input topology if:
|
1040
|
-
01. Vertex: If it is internal to it (i.e. coincident with it).
|
1041
|
-
02. Edge: If it is internal to its start or end vertices.
|
1042
|
-
03. Manifold open wire: If it is internal to its start or end vertices.
|
1043
|
-
04. Manifold closed wire: If it is internal to any of its vertices.
|
1044
|
-
05. Non-manifold wire: If it is internal to any of its vertices that has a vertex degree of 1.
|
1045
|
-
06. Face: If it is internal to any of its edges or vertices.
|
1046
|
-
07. Shell: If it is internal to external boundary
|
1047
|
-
08. Cell: If it is internal to any of its faces, edges, or vertices.
|
1048
|
-
09. CellComplex: If it is peripheral to its external boundary.
|
1049
|
-
10. Cluster: If it is peripheral to any of its free topologies. (See Cluster.FreeTopologies)
|
1050
|
-
|
1051
|
-
Parameters
|
1052
|
-
----------
|
1053
|
-
vertex : topologic.Vertex
|
1054
|
-
The input vertex.
|
1055
|
-
topology : topologic.Topology
|
1056
|
-
The input topology.
|
1057
|
-
tolerance : float , optional
|
1058
|
-
The tolerance for computing if the input vertex is peripheral to the input topology. The default is 0.0001.
|
1059
|
-
silent : bool , optional
|
1060
|
-
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
|
1061
|
-
|
1062
|
-
Returns
|
1063
|
-
-------
|
1064
|
-
bool
|
1065
|
-
True if the input vertex is peripheral to the input topology. False otherwise.
|
1066
|
-
|
1067
|
-
"""
|
1068
|
-
from topologicpy.Edge import Edge
|
1069
|
-
from topologicpy.Wire import Wire
|
1070
|
-
from topologicpy.Face import Face
|
1071
|
-
from topologicpy.Shell import Shell
|
1072
|
-
from topologicpy.CellComplex import CellComplex
|
1073
|
-
from topologicpy.Cluster import Cluster
|
1074
|
-
from topologicpy.Topology import Topology
|
1075
|
-
|
1076
|
-
if not isinstance(vertex, topologic.Vertex):
|
1077
|
-
if not silent:
|
1078
|
-
print("Vertex.IsPeripheral - Error: The input vertex parameter is not a valid vertex. Returning None.")
|
1079
|
-
return None
|
1080
|
-
if not isinstance(topology, topologic.Topology):
|
1081
|
-
if not silent:
|
1082
|
-
print("Vertex.IsPeripheral - Error: The input topology parameter is not a valid topology. Returning None.")
|
1083
|
-
return None
|
1084
|
-
|
1085
|
-
if isinstance(topology, topologic.Vertex):
|
1086
|
-
return Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent)
|
1087
|
-
elif isinstance(topology, topologic.Edge):
|
1088
|
-
sv = Edge.StartVertex(topology)
|
1089
|
-
ev = Edge.EndVertex(topology)
|
1090
|
-
f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
|
1091
|
-
f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
|
1092
|
-
return f1 or f2
|
1093
|
-
elif isinstance(topology, topologic.Wire):
|
1094
|
-
if Wire.IsManifold(topology):
|
1095
|
-
if not Wire.IsClosed(topology):
|
1096
|
-
sv = Wire.StartVertex(topology)
|
1097
|
-
ev = Wire.EndVertex(topology)
|
1098
|
-
f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
|
1099
|
-
f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
|
1100
|
-
return f1 or f2
|
1101
|
-
else:
|
1102
|
-
sub_list = [v for v in Topology.Vertices(topology)]
|
1103
|
-
for sub in sub_list:
|
1104
|
-
if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
|
1105
|
-
return True
|
1106
|
-
return False
|
1107
|
-
else:
|
1108
|
-
sub_list = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) == 1]
|
1109
|
-
for sub in sub_list:
|
1110
|
-
if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
|
1111
|
-
return True
|
1112
|
-
return False
|
1113
|
-
elif isinstance(topology, topologic.Face):
|
1114
|
-
sub_list = Topology.Vertices(topology) + Topology.Edges(topology)
|
1115
|
-
for sub in sub_list:
|
1116
|
-
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1117
|
-
return True
|
1118
|
-
return False
|
1119
|
-
elif isinstance(topology, topologic.Shell):
|
1120
|
-
ext_boundary = Shell.ExternalBoundary(topology)
|
1121
|
-
sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary)
|
1122
|
-
for sub in sub_list:
|
1123
|
-
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1124
|
-
return True
|
1125
|
-
return False
|
1126
|
-
elif isinstance(topology, topologic.Cell):
|
1127
|
-
sub_list = Topology.Vertices(topology) + Topology.Edges(topology) + Topology.Faces(topology)
|
1128
|
-
for sub in sub_list:
|
1129
|
-
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1130
|
-
return True
|
1131
|
-
return False
|
1132
|
-
elif isinstance(topology, topologic.CellComplex):
|
1133
|
-
ext_boundary = CellComplex.ExternalBoundary(topology)
|
1134
|
-
sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary) + Topology.Faces(ext_boundary)
|
1135
|
-
for sub in sub_list:
|
1136
|
-
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1137
|
-
return True
|
1138
|
-
return False
|
1139
|
-
elif isinstance(topology, topologic.Cluster):
|
1140
|
-
sub_list = Cluster.FreeTopologies(topology)
|
1141
|
-
for sub in sub_list:
|
1142
|
-
if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
|
1143
|
-
return True
|
1144
|
-
return False
|
1145
|
-
return False
|
1146
|
-
|
1147
|
-
@staticmethod
|
1148
|
-
def NearestVertex(vertex: topologic.Vertex, topology: topologic.Topology, useKDTree: bool = True) -> topologic.Vertex:
|
1149
|
-
"""
|
1150
|
-
Returns the vertex found in the input topology that is the nearest to the input vertex.
|
1151
|
-
|
1152
|
-
Parameters
|
1153
|
-
----------
|
1154
|
-
vertex : topologic.Vertex
|
1155
|
-
The input vertex.
|
1156
|
-
topology : topologic.Topology
|
1157
|
-
The input topology to be searched for the nearest vertex.
|
1158
|
-
useKDTree : bool , optional
|
1159
|
-
if set to True, the algorithm will use a KDTree method to search for the nearest vertex. The default is True.
|
1160
|
-
|
1161
|
-
Returns
|
1162
|
-
-------
|
1163
|
-
topologic.Vertex
|
1164
|
-
The nearest vertex.
|
1165
|
-
|
1166
|
-
"""
|
1167
|
-
def SED(a, b):
|
1168
|
-
"""Compute the squared Euclidean distance between X and Y."""
|
1169
|
-
p1 = (a.X(), a.Y(), a.Z())
|
1170
|
-
p2 = (b.X(), b.Y(), b.Z())
|
1171
|
-
return sum((i-j)**2 for i, j in zip(p1, p2))
|
1172
|
-
|
1173
|
-
BT = collections.namedtuple("BT", ["value", "left", "right"])
|
1174
|
-
BT.__doc__ = """
|
1175
|
-
A Binary Tree (BT) with a node value, and left- and
|
1176
|
-
right-subtrees.
|
1177
|
-
"""
|
1178
|
-
def firstItem(v):
|
1179
|
-
return v.X()
|
1180
|
-
def secondItem(v):
|
1181
|
-
return v.Y()
|
1182
|
-
def thirdItem(v):
|
1183
|
-
return v.Z()
|
1184
|
-
|
1185
|
-
def itemAtIndex(v, index):
|
1186
|
-
if index == 0:
|
1187
|
-
return v.X()
|
1188
|
-
elif index == 1:
|
1189
|
-
return v.Y()
|
1190
|
-
elif index == 2:
|
1191
|
-
return v.Z()
|
1192
|
-
|
1193
|
-
def sortList(vertices, index):
|
1194
|
-
if index == 0:
|
1195
|
-
vertices.sort(key=firstItem)
|
1196
|
-
elif index == 1:
|
1197
|
-
vertices.sort(key=secondItem)
|
1198
|
-
elif index == 2:
|
1199
|
-
vertices.sort(key=thirdItem)
|
1200
|
-
return vertices
|
1201
|
-
|
1202
|
-
def kdtree(topology):
|
1203
|
-
assert isinstance(topology, topologic.Topology), "Vertex.NearestVertex: The input is not a Topology."
|
1204
|
-
vertices = []
|
1205
|
-
_ = topology.Vertices(None, vertices)
|
1206
|
-
assert (len(vertices) > 0), "Vertex.NearestVertex: Could not find any vertices in the input Topology"
|
1207
|
-
|
1208
|
-
"""Construct a k-d tree from an iterable of vertices.
|
1209
|
-
|
1210
|
-
This algorithm is taken from Wikipedia. For more details,
|
1211
|
-
|
1212
|
-
> https://en.wikipedia.org/wiki/K-d_tree#Construction
|
1213
|
-
|
1214
|
-
"""
|
1215
|
-
# k = len(points[0])
|
1216
|
-
k = 3
|
1217
|
-
|
1218
|
-
def build(*, vertices, depth):
|
1219
|
-
if len(vertices) == 0:
|
1220
|
-
return None
|
1221
|
-
#points.sort(key=operator.itemgetter(depth % k))
|
1222
|
-
vertices = sortList(vertices, (depth % k))
|
1223
|
-
|
1224
|
-
middle = len(vertices) // 2
|
1225
|
-
|
1226
|
-
return BT(
|
1227
|
-
value = vertices[middle],
|
1228
|
-
left = build(
|
1229
|
-
vertices=vertices[:middle],
|
1230
|
-
depth=depth+1,
|
1231
|
-
),
|
1232
|
-
right = build(
|
1233
|
-
vertices=vertices[middle+1:],
|
1234
|
-
depth=depth+1,
|
1235
|
-
),
|
1236
|
-
)
|
1237
|
-
|
1238
|
-
return build(vertices=list(vertices), depth=0)
|
1239
|
-
|
1240
|
-
NNRecord = collections.namedtuple("NNRecord", ["vertex", "distance"])
|
1241
|
-
NNRecord.__doc__ = """
|
1242
|
-
Used to keep track of the current best guess during a nearest
|
1243
|
-
neighbor search.
|
1244
|
-
"""
|
1245
|
-
|
1246
|
-
def find_nearest_neighbor(*, tree, vertex):
|
1247
|
-
"""Find the nearest neighbor in a k-d tree for a given vertex.
|
1248
|
-
"""
|
1249
|
-
k = 3 # Forcing k to be 3 dimensional
|
1250
|
-
best = None
|
1251
|
-
def search(*, tree, depth):
|
1252
|
-
"""Recursively search through the k-d tree to find the nearest neighbor.
|
1253
|
-
"""
|
1254
|
-
nonlocal best
|
1255
|
-
|
1256
|
-
if tree is None:
|
1257
|
-
return
|
1258
|
-
distance = SED(tree.value, vertex)
|
1259
|
-
if best is None or distance < best.distance:
|
1260
|
-
best = NNRecord(vertex=tree.value, distance=distance)
|
1261
|
-
|
1262
|
-
axis = depth % k
|
1263
|
-
diff = itemAtIndex(vertex,axis) - itemAtIndex(tree.value,axis)
|
1264
|
-
if diff <= 0:
|
1265
|
-
close, away = tree.left, tree.right
|
1266
|
-
else:
|
1267
|
-
close, away = tree.right, tree.left
|
1268
|
-
|
1269
|
-
search(tree=close, depth=depth+1)
|
1270
|
-
if diff**2 < best.distance:
|
1271
|
-
search(tree=away, depth=depth+1)
|
1272
|
-
|
1273
|
-
search(tree=tree, depth=0)
|
1274
|
-
return best.vertex
|
1275
|
-
|
1276
|
-
if useKDTree:
|
1277
|
-
tree = kdtree(topology)
|
1278
|
-
return find_nearest_neighbor(tree=tree, vertex=vertex)
|
1279
|
-
else:
|
1280
|
-
vertices = []
|
1281
|
-
_ = topology.Vertices(None, vertices)
|
1282
|
-
distances = []
|
1283
|
-
indices = []
|
1284
|
-
for i in range(len(vertices)):
|
1285
|
-
distances.append(SED(vertex, vertices[i]))
|
1286
|
-
indices.append(i)
|
1287
|
-
sorted_indices = [x for _, x in sorted(zip(distances, indices))]
|
1288
|
-
return vertices[sorted_indices[0]]
|
1289
|
-
|
1290
|
-
@staticmethod
|
1291
|
-
def Origin() -> topologic.Vertex:
|
1292
|
-
"""
|
1293
|
-
Returns a vertex with coordinates (0, 0, 0)
|
1294
|
-
|
1295
|
-
Parameters
|
1296
|
-
-----------
|
1297
|
-
|
1298
|
-
Return
|
1299
|
-
-----------
|
1300
|
-
topologic.Vertex
|
1301
|
-
"""
|
1302
|
-
return Vertex.ByCoordinates(0, 0, 0)
|
1303
|
-
|
1304
|
-
@staticmethod
|
1305
|
-
def PerpendicularDistance(vertex: topologic.Vertex, face: topologic.Face, mantissa: int = 6):
|
1306
|
-
"""
|
1307
|
-
Returns the perpendicular distance between the input vertex and the input face. The face is considered to be infinite.
|
1308
|
-
|
1309
|
-
Parameters
|
1310
|
-
----------
|
1311
|
-
vertex : topologic.Vertex
|
1312
|
-
The input vertex.
|
1313
|
-
face : topologic.Face
|
1314
|
-
The input face.
|
1315
|
-
mantissa: int , optional
|
1316
|
-
The desired length of the mantissa. The default is 6.
|
1317
|
-
|
1318
|
-
Returns
|
1319
|
-
-------
|
1320
|
-
float
|
1321
|
-
The distance between the input vertex and the input topology.
|
1322
|
-
|
1323
|
-
"""
|
1324
|
-
from topologicpy.Face import Face
|
1325
|
-
import math
|
1326
|
-
|
1327
|
-
def distance_point_to_line(point, line_start, line_end):
|
1328
|
-
# Convert input points to NumPy arrays for vector operations
|
1329
|
-
point = np.array(point)
|
1330
|
-
line_start = np.array(line_start)
|
1331
|
-
line_end = np.array(line_end)
|
1332
|
-
|
1333
|
-
# Calculate the direction vector of the edge
|
1334
|
-
line_direction = line_end - line_start
|
1335
|
-
|
1336
|
-
# Vector from the edge's starting point to the point
|
1337
|
-
point_to_start = point - line_start
|
1338
|
-
|
1339
|
-
# Calculate the parameter 't' where the projection of the point onto the edge occurs
|
1340
|
-
if np.dot(line_direction, line_direction) == 0:
|
1341
|
-
t = 0
|
1342
|
-
else:
|
1343
|
-
t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
|
1344
|
-
|
1345
|
-
# Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
|
1346
|
-
if t < 0:
|
1347
|
-
return np.linalg.norm(point - line_start)
|
1348
|
-
elif t > 1:
|
1349
|
-
return np.linalg.norm(point - line_end)
|
1350
|
-
|
1351
|
-
# Calculate the closest point on the edge to the given point
|
1352
|
-
closest_point = line_start + t * line_direction
|
1353
|
-
|
1354
|
-
# Calculate the distance between the closest point and the given point
|
1355
|
-
distance = np.linalg.norm(point - closest_point)
|
1356
|
-
|
1357
|
-
return distance
|
1358
|
-
if not isinstance(vertex, topologic.Vertex):
|
1359
|
-
print("Vertex.PerpendicularDistance - Error: The input vertex is not a valid topologic vertex. Returning None.")
|
1360
|
-
return None
|
1361
|
-
if not isinstance(face, topologic.Face):
|
1362
|
-
print("Vertex.PerpendicularDistance - Error: The input face is not a valid topologic face. Returning None.")
|
1363
|
-
return None
|
1364
|
-
dic = Face.PlaneEquation(face)
|
1365
|
-
if dic == None: # The face is degenerate. Try to treat as an edge.
|
1366
|
-
point = Vertex.Coordinates(vertex)
|
1367
|
-
face_vertices = Topology.Vertices(face)
|
1368
|
-
line_start = Vertex.Coordinates(face_vertices[0])
|
1369
|
-
line_end = Vertex.Coordinates(face_vertices[1])
|
1370
|
-
return round(distance_point_to_line(point, line_start, line_end), mantissa)
|
1371
|
-
a = dic["a"]
|
1372
|
-
b = dic["b"]
|
1373
|
-
c = dic["c"]
|
1374
|
-
d = dic["d"]
|
1375
|
-
x1, y1, z1 = Vertex.Coordinates(vertex)
|
1376
|
-
d = abs((a * x1 + b * y1 + c * z1 + d))
|
1377
|
-
e = (math.sqrt(a * a + b * b + c * c))
|
1378
|
-
if e == 0:
|
1379
|
-
return 0
|
1380
|
-
return round(d/e, mantissa)
|
1381
|
-
|
1382
|
-
@staticmethod
|
1383
|
-
def PlaneEquation(vertices, mantissa: int = 6):
|
1384
|
-
"""
|
1385
|
-
Returns the equation of the average plane passing through a list of vertices.
|
1386
|
-
|
1387
|
-
Parameters
|
1388
|
-
-----------
|
1389
|
-
vertices : list
|
1390
|
-
The input list of vertices
|
1391
|
-
mantissa : int , optional
|
1392
|
-
The desired length of the mantissa. The default is 6.
|
1393
|
-
|
1394
|
-
Return
|
1395
|
-
-----------
|
1396
|
-
dict
|
1397
|
-
The dictionary containing the values of a, b, c, d for the plane equation in the form of ax+by+cz+d=0.
|
1398
|
-
The keys in the dictionary are ["a", "b", "c". "d"]
|
1399
|
-
"""
|
1400
|
-
|
1401
|
-
vertices = [Vertex.Coordinates(v) for v in vertices]
|
1402
|
-
# Convert vertices to a NumPy array for easier calculations
|
1403
|
-
vertices = np.array(vertices)
|
1404
|
-
|
1405
|
-
# Calculate the centroid of the vertices
|
1406
|
-
centroid = np.mean(vertices, axis=0)
|
1407
|
-
|
1408
|
-
# Center the vertices by subtracting the centroid
|
1409
|
-
centered_vertices = vertices - centroid
|
1410
|
-
|
1411
|
-
# Calculate the covariance matrix
|
1412
|
-
covariance_matrix = np.dot(centered_vertices.T, centered_vertices)
|
1413
|
-
|
1414
|
-
# Find the normal vector by computing the eigenvector of the smallest eigenvalue
|
1415
|
-
_, eigen_vectors = np.linalg.eigh(covariance_matrix)
|
1416
|
-
normal_vector = eigen_vectors[:, 0]
|
1417
|
-
|
1418
|
-
# Normalize the normal vector
|
1419
|
-
normal_vector /= np.linalg.norm(normal_vector)
|
1420
|
-
|
1421
|
-
# Calculate the constant D using the centroid and the normal vector
|
1422
|
-
d = -np.dot(normal_vector, centroid)
|
1423
|
-
d = round(d, mantissa)
|
1424
|
-
|
1425
|
-
# Create the plane equation in the form Ax + By + Cz + D = 0
|
1426
|
-
a, b, c = normal_vector
|
1427
|
-
a = round(a, mantissa)
|
1428
|
-
b = round(b, mantissa)
|
1429
|
-
c = round(c, mantissa)
|
1430
|
-
|
1431
|
-
return {"a":a, "b":b, "c":c, "d":d}
|
1432
|
-
|
1433
|
-
@staticmethod
|
1434
|
-
def Point(x=0, y=0, z=0) -> topologic.Vertex:
|
1435
|
-
"""
|
1436
|
-
Creates a point (vertex) using the input parameters
|
1437
|
-
|
1438
|
-
Parameters
|
1439
|
-
-----------
|
1440
|
-
x : float , optional.
|
1441
|
-
The desired x coordinate. The default is 0.
|
1442
|
-
y : float , optional.
|
1443
|
-
The desired y coordinate. The default is 0.
|
1444
|
-
z : float , optional.
|
1445
|
-
The desired z coordinate. The default is 0.
|
1446
|
-
|
1447
|
-
Return
|
1448
|
-
-----------
|
1449
|
-
topologic.Vertex
|
1450
|
-
"""
|
1451
|
-
|
1452
|
-
return Vertex.ByCoordinates(x, y, z)
|
1453
|
-
|
1454
|
-
@staticmethod
|
1455
|
-
def Project(vertex: topologic.Vertex, face: topologic.Face, direction: bool = None, mantissa: int = 6) -> topologic.Vertex:
|
1456
|
-
"""
|
1457
|
-
Returns a vertex that is the projection of the input vertex unto the input face.
|
1458
|
-
|
1459
|
-
Parameters
|
1460
|
-
----------
|
1461
|
-
vertex : topologic.Vertex
|
1462
|
-
The input vertex to project unto the input face.
|
1463
|
-
face : topologic.Face
|
1464
|
-
The input face that receives the projection of the input vertex.
|
1465
|
-
direction : vector, optional
|
1466
|
-
The direction in which to project the input vertex unto the input face. If not specified, the direction of the projection is the normal of the input face. The default is None.
|
1467
|
-
mantissa : int , optional
|
1468
|
-
The length of the desired mantissa. The default is 6.
|
1469
|
-
tolerance : float , optional
|
1470
|
-
The desired tolerance. The default is 0.0001.
|
1471
|
-
|
1472
|
-
Returns
|
1473
|
-
-------
|
1474
|
-
topologic.Vertex
|
1475
|
-
The projected vertex.
|
1476
|
-
|
1477
|
-
"""
|
1478
|
-
from topologicpy.Face import Face
|
1479
|
-
|
1480
|
-
def project_point_onto_plane(point, plane_coeffs, direction_vector):
|
1481
|
-
"""
|
1482
|
-
Project a 3D point onto a plane defined by its coefficients and using a direction vector.
|
1483
|
-
|
1484
|
-
Parameters:
|
1485
|
-
point (tuple or list): The 3D point coordinates (x, y, z).
|
1486
|
-
plane_coeffs (tuple or list): The coefficients of the plane equation (a, b, c, d).
|
1487
|
-
direction_vector (tuple or list): The direction vector (vx, vy, vz).
|
1488
|
-
|
1489
|
-
Returns:
|
1490
|
-
tuple: The projected point coordinates (x_proj, y_proj, z_proj).
|
1491
|
-
"""
|
1492
|
-
# Unpack point coordinates
|
1493
|
-
x, y, z = point
|
1494
|
-
|
1495
|
-
# Unpack plane coefficients
|
1496
|
-
a, b, c, d = plane_coeffs
|
1497
|
-
|
1498
|
-
# Unpack direction vector
|
1499
|
-
vx, vy, vz = direction_vector
|
1500
|
-
|
1501
|
-
# Calculate the distance from the point to the plane
|
1502
|
-
distance = (a * x + b * y + c * z + d) / (a * vx + b * vy + c * vz)
|
1503
|
-
|
1504
|
-
# Calculate the projected point coordinates
|
1505
|
-
x_proj = x - distance * vx
|
1506
|
-
y_proj = y - distance * vy
|
1507
|
-
z_proj = z - distance * vz
|
1508
|
-
|
1509
|
-
return [x_proj, y_proj, z_proj]
|
1510
|
-
|
1511
|
-
if not isinstance(vertex, topologic.Vertex):
|
1512
|
-
return None
|
1513
|
-
if not isinstance(face, topologic.Face):
|
1514
|
-
return None
|
1515
|
-
eq = Face.PlaneEquation(face, mantissa= mantissa)
|
1516
|
-
if direction == None or direction == []:
|
1517
|
-
direction = Face.Normal(face)
|
1518
|
-
pt = project_point_onto_plane(Vertex.Coordinates(vertex), [eq["a"], eq["b"], eq["c"], eq["d"]], direction)
|
1519
|
-
return Vertex.ByCoordinates(pt[0], pt[1], pt[2])
|
1520
|
-
|
1521
|
-
@staticmethod
|
1522
|
-
def X(vertex: topologic.Vertex, mantissa: int = 6) -> float:
|
1523
|
-
"""
|
1524
|
-
Returns the X coordinate of the input vertex.
|
1525
|
-
|
1526
|
-
Parameters
|
1527
|
-
----------
|
1528
|
-
vertex : topologic.Vertex
|
1529
|
-
The input vertex.
|
1530
|
-
mantissa : int , optional
|
1531
|
-
The desired length of the mantissa. The default is 6.
|
1532
|
-
|
1533
|
-
Returns
|
1534
|
-
-------
|
1535
|
-
float
|
1536
|
-
The X coordinate of the input vertex.
|
1537
|
-
|
1538
|
-
"""
|
1539
|
-
if not isinstance(vertex, topologic.Vertex):
|
1540
|
-
return None
|
1541
|
-
return round(vertex.X(), mantissa)
|
1542
|
-
|
1543
|
-
@staticmethod
|
1544
|
-
def Y(vertex: topologic.Vertex, mantissa: int = 6) -> float:
|
1545
|
-
"""
|
1546
|
-
Returns the Y coordinate of the input vertex.
|
1547
|
-
|
1548
|
-
Parameters
|
1549
|
-
----------
|
1550
|
-
vertex : topologic.Vertex
|
1551
|
-
The input vertex.
|
1552
|
-
mantissa : int , optional
|
1553
|
-
The desired length of the mantissa. The default is 6.
|
1554
|
-
|
1555
|
-
Returns
|
1556
|
-
-------
|
1557
|
-
float
|
1558
|
-
The Y coordinate of the input vertex.
|
1559
|
-
|
1560
|
-
"""
|
1561
|
-
if not isinstance(vertex, topologic.Vertex):
|
1562
|
-
return None
|
1563
|
-
return round(vertex.Y(), mantissa)
|
1564
|
-
|
1565
|
-
@staticmethod
|
1566
|
-
def Z(vertex: topologic.Vertex, mantissa: int = 6) -> float:
|
1567
|
-
"""
|
1568
|
-
Returns the Z coordinate of the input vertex.
|
1569
|
-
|
1570
|
-
Parameters
|
1571
|
-
----------
|
1572
|
-
vertex : topologic.Vertex
|
1573
|
-
The input vertex.
|
1574
|
-
mantissa : int , optional
|
1575
|
-
The desired length of the mantissa. The default is 6.
|
1576
|
-
|
1577
|
-
Returns
|
1578
|
-
-------
|
1579
|
-
float
|
1580
|
-
The Z coordinate of the input vertex.
|
1581
|
-
|
1582
|
-
"""
|
1583
|
-
if not isinstance(vertex, topologic.Vertex):
|
1584
|
-
return None
|
1585
|
-
return round(vertex.Z(), mantissa)
|
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 topologicpy
|
18
|
+
import topologic_core as topologic
|
19
|
+
from topologicpy.Face import Face
|
20
|
+
from topologicpy.Topology import Topology
|
21
|
+
import collections
|
22
|
+
import os
|
23
|
+
import warnings
|
24
|
+
|
25
|
+
try:
|
26
|
+
import numpy as np
|
27
|
+
except:
|
28
|
+
print("Vertex - Installing required numpy library.")
|
29
|
+
try:
|
30
|
+
os.system("pip install numpy")
|
31
|
+
except:
|
32
|
+
os.system("pip install numpy --user")
|
33
|
+
try:
|
34
|
+
import numpy as np
|
35
|
+
print("Vertex - numpy library installed successfully.")
|
36
|
+
except:
|
37
|
+
warnings.warn("Vertex - Error: Could not import numpy.")
|
38
|
+
|
39
|
+
class Vertex(Topology):
|
40
|
+
@staticmethod
|
41
|
+
def AreCollinear(vertices: list, tolerance: float = 0.0001):
|
42
|
+
"""
|
43
|
+
Returns True if the input list of vertices form a straight line. Returns False otherwise.
|
44
|
+
|
45
|
+
Parameters
|
46
|
+
----------
|
47
|
+
vertices : list
|
48
|
+
The input list of vertices.
|
49
|
+
tolerance : float, optional
|
50
|
+
The desired tolerance. The default is 0.0001.
|
51
|
+
|
52
|
+
Returns
|
53
|
+
-------
|
54
|
+
bool
|
55
|
+
True if the input vertices are on the same side of the face. False otherwise.
|
56
|
+
|
57
|
+
"""
|
58
|
+
from topologicpy.Cluster import Cluster
|
59
|
+
from topologicpy.Topology import Topology
|
60
|
+
from topologicpy.Vector import Vector
|
61
|
+
import sys
|
62
|
+
def areCollinear(vertices, tolerance=0.0001):
|
63
|
+
point1 = [Vertex.X(vertices[0]), Vertex.Y(vertices[0]), Vertex.Z(vertices[0])]
|
64
|
+
point2 = [Vertex.X(vertices[1]), Vertex.Y(vertices[1]), Vertex.Z(vertices[1])]
|
65
|
+
point3 = [Vertex.X(vertices[2]), Vertex.Y(vertices[2]), Vertex.Z(vertices[2])]
|
66
|
+
|
67
|
+
vector1 = [point2[0] - point1[0], point2[1] - point1[1], point2[2] - point1[2]]
|
68
|
+
vector2 = [point3[0] - point1[0], point3[1] - point1[1], point3[2] - point1[2]]
|
69
|
+
|
70
|
+
cross_product_result = Vector.Cross(vector1, vector2, tolerance=tolerance)
|
71
|
+
return cross_product_result == None
|
72
|
+
|
73
|
+
if not isinstance(vertices, list):
|
74
|
+
print("Vertex.AreCollinear - Error: The input list of vertices is not a valid list. Returning None.")
|
75
|
+
return None
|
76
|
+
vertexList = [x for x in vertices if isinstance(x, topologic.Vertex)]
|
77
|
+
if len(vertexList) < 2:
|
78
|
+
print("Vertex.AreCollinear - Error: The input list of vertices does not contain sufficient valid vertices. Returning None.")
|
79
|
+
return None
|
80
|
+
if len(vertexList) < 3:
|
81
|
+
return True # Any two vertices can form a line!
|
82
|
+
cluster = Topology.SelfMerge(Cluster.ByTopologies(vertexList), tolerance=tolerance)
|
83
|
+
vertexList = Topology.Vertices(cluster)
|
84
|
+
slices = []
|
85
|
+
for i in range(2,len(vertexList)):
|
86
|
+
slices.append([vertexList[0], vertexList[1], vertexList[i]])
|
87
|
+
for slice in slices:
|
88
|
+
if not areCollinear(slice, tolerance=tolerance):
|
89
|
+
return False
|
90
|
+
return True
|
91
|
+
|
92
|
+
@staticmethod
|
93
|
+
def AreIpsilateral(vertices: list, face: topologic.Face) -> bool:
|
94
|
+
"""
|
95
|
+
Returns True if the input list of vertices are on one side of a face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
96
|
+
|
97
|
+
Parameters
|
98
|
+
----------
|
99
|
+
vertices : list
|
100
|
+
The input list of vertices.
|
101
|
+
face : topologic.Face
|
102
|
+
The input face
|
103
|
+
|
104
|
+
Returns
|
105
|
+
-------
|
106
|
+
bool
|
107
|
+
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
108
|
+
|
109
|
+
"""
|
110
|
+
def check(dot_productA, pointB, pointC, normal):
|
111
|
+
# Calculate the dot products of the vectors from the surface point to each of the input points.
|
112
|
+
dot_productB = (pointB[0] - pointC[0]) * normal[0] + \
|
113
|
+
(pointB[1] - pointC[1]) * normal[1] + \
|
114
|
+
(pointB[2] - pointC[2]) * normal[2]
|
115
|
+
|
116
|
+
# Check if both points are on the same side of the surface.
|
117
|
+
if dot_productA * dot_productB > 0:
|
118
|
+
return True
|
119
|
+
|
120
|
+
# Check if both points are on opposite sides of the surface.
|
121
|
+
elif dot_productA * dot_productB < 0:
|
122
|
+
return False
|
123
|
+
|
124
|
+
# Otherwise, at least one point is on the surface.
|
125
|
+
else:
|
126
|
+
return True
|
127
|
+
|
128
|
+
from topologicpy.Vertex import Vertex
|
129
|
+
from topologicpy.Face import Face
|
130
|
+
|
131
|
+
if not isinstance(face, topologic.Face):
|
132
|
+
return None
|
133
|
+
vertexList = [x for x in vertices if isinstance(x, topologic.Vertex)]
|
134
|
+
if len(vertexList) < 2:
|
135
|
+
return None
|
136
|
+
pointA = Vertex.Coordinates(vertexList[0])
|
137
|
+
pointC = Vertex.Coordinates(Face.VertexByParameters(face, 0.5, 0.5))
|
138
|
+
normal = Face.Normal(face)
|
139
|
+
dot_productA = (pointA[0] - pointC[0]) * normal[0] + \
|
140
|
+
(pointA[1] - pointC[1]) * normal[1] + \
|
141
|
+
(pointA[2] - pointC[2]) * normal[2]
|
142
|
+
for i in range(1, len(vertexList)):
|
143
|
+
pointB = Vertex.Coordinates(vertexList[i])
|
144
|
+
if not check(dot_productA, pointB, pointC, normal):
|
145
|
+
return False
|
146
|
+
return True
|
147
|
+
|
148
|
+
@staticmethod
|
149
|
+
def AreIpsilateralCluster(cluster: topologic.Cluster, face: topologic.Face) -> bool:
|
150
|
+
"""
|
151
|
+
Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
152
|
+
|
153
|
+
Parameters
|
154
|
+
----------
|
155
|
+
cluster : topologic.Cluster
|
156
|
+
The input list of vertices.
|
157
|
+
face : topologic.Face
|
158
|
+
The input face
|
159
|
+
|
160
|
+
Returns
|
161
|
+
-------
|
162
|
+
bool
|
163
|
+
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
164
|
+
|
165
|
+
"""
|
166
|
+
from topologicpy.Topology import Topology
|
167
|
+
if not isinstance(cluster, topologic.Topology):
|
168
|
+
return None
|
169
|
+
vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
|
170
|
+
return Vertex.AreIpsilateral(vertices, face)
|
171
|
+
|
172
|
+
@staticmethod
|
173
|
+
def AreOnSameSide(vertices: list, face: topologicpy.Face.Face) -> bool:
|
174
|
+
"""
|
175
|
+
Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
176
|
+
|
177
|
+
Parameters
|
178
|
+
----------
|
179
|
+
vertices : list
|
180
|
+
The input list of vertices.
|
181
|
+
face : topologic.Face
|
182
|
+
The input face
|
183
|
+
|
184
|
+
Returns
|
185
|
+
-------
|
186
|
+
bool
|
187
|
+
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
188
|
+
|
189
|
+
"""
|
190
|
+
return Vertex.AreIpsilateral(vertices, face)
|
191
|
+
|
192
|
+
@staticmethod
|
193
|
+
def AreOnSameSideCluster(cluster: topologic.Cluster, face: topologic.Face) -> bool:
|
194
|
+
"""
|
195
|
+
Returns True if the two input vertices are on the same side of the input face. Returns False otherwise. If at least one of the vertices is on the face, this method return True.
|
196
|
+
|
197
|
+
Parameters
|
198
|
+
----------
|
199
|
+
cluster : topologic.Cluster
|
200
|
+
The input list of vertices.
|
201
|
+
face : topologic.Face
|
202
|
+
The input face
|
203
|
+
|
204
|
+
Returns
|
205
|
+
-------
|
206
|
+
bool
|
207
|
+
True if the input vertices are on the same side of the face. False otherwise. If at least one of the vertices is on the face, this method return True.
|
208
|
+
|
209
|
+
"""
|
210
|
+
from topologicpy.Topology import Topology
|
211
|
+
if not isinstance(cluster, topologic.Topology):
|
212
|
+
return None
|
213
|
+
vertices = Topology.SubTopologies(cluster, subTopologyType="vertex")
|
214
|
+
return Vertex.AreIpsilateral(vertices, face)
|
215
|
+
|
216
|
+
@staticmethod
|
217
|
+
def ByCoordinates(*args, **kwargs) -> topologic.Vertex:
|
218
|
+
"""
|
219
|
+
Creates a vertex at the coordinates specified by the x, y, z inputs. You can call this method using a list of coordinates or individually.
|
220
|
+
Examples:
|
221
|
+
v = Vertex.ByCoordinates(3.4, 5.7, 2.8)
|
222
|
+
v = Vertex.ByCoordinates([3.4, 5.7, 2.8])
|
223
|
+
v = Vertex.ByCoordinates(x=3.4, y=5.7, z=2.8)
|
224
|
+
|
225
|
+
Parameters
|
226
|
+
----------
|
227
|
+
x : float , optional
|
228
|
+
The X coordinate. The default is 0.
|
229
|
+
y : float , optional
|
230
|
+
The Y coordinate. The default is 0.
|
231
|
+
z : float , optional
|
232
|
+
The Z coordinate. The defaults is 0.
|
233
|
+
|
234
|
+
Returns
|
235
|
+
-------
|
236
|
+
topologic.Vertex
|
237
|
+
The created vertex.
|
238
|
+
|
239
|
+
"""
|
240
|
+
import numbers
|
241
|
+
x = None
|
242
|
+
y = None
|
243
|
+
z = None
|
244
|
+
if len(args) > 3 or len(kwargs.items()) > 3:
|
245
|
+
print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
|
246
|
+
return None
|
247
|
+
if len(args) > 0:
|
248
|
+
value = args[0]
|
249
|
+
if isinstance(value, list) and len(value) > 3:
|
250
|
+
print("Vertex.ByCoordinates - Error: Input parameters are greater than 3. Returning None.")
|
251
|
+
return None
|
252
|
+
elif isinstance(value, list) and len(value) == 3:
|
253
|
+
x = value[0]
|
254
|
+
y = value[1]
|
255
|
+
z = value[2]
|
256
|
+
elif isinstance(value, list) and len(value) == 2:
|
257
|
+
x = value[0]
|
258
|
+
y = value[1]
|
259
|
+
elif isinstance(value, list) and len(value) == 1:
|
260
|
+
x = value[0]
|
261
|
+
elif len(args) == 3:
|
262
|
+
x = args[0]
|
263
|
+
y = args[1]
|
264
|
+
z = args[2]
|
265
|
+
elif len(args) == 2:
|
266
|
+
x = args[0]
|
267
|
+
y = args[1]
|
268
|
+
elif len(args) == 1:
|
269
|
+
x = args[0]
|
270
|
+
for key, value in kwargs.items():
|
271
|
+
if "x" in key.lower():
|
272
|
+
if not x == None:
|
273
|
+
print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
|
274
|
+
return None
|
275
|
+
x = value
|
276
|
+
elif "y" in key.lower():
|
277
|
+
if not y == None:
|
278
|
+
print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
|
279
|
+
return None
|
280
|
+
y = value
|
281
|
+
elif "z" in key.lower():
|
282
|
+
if not z == None:
|
283
|
+
print("Vertex.ByCoordinates - Error: Input parameters are not formed properly. Returning None.")
|
284
|
+
return None
|
285
|
+
z = value
|
286
|
+
if x == None:
|
287
|
+
x = 0
|
288
|
+
if y == None:
|
289
|
+
y = 0
|
290
|
+
if z == None:
|
291
|
+
z = 0
|
292
|
+
if not isinstance(x, numbers.Number):
|
293
|
+
print("Vertex.ByCoordinates - Error: The x value is not a valid number. Returning None.")
|
294
|
+
return None
|
295
|
+
if not isinstance(y, numbers.Number):
|
296
|
+
print("Vertex.ByCoordinates - Error: The y value is not a valid number. Returning None.")
|
297
|
+
return None
|
298
|
+
if not isinstance(z, numbers.Number):
|
299
|
+
print("Vertex.ByCoordinates - Error: The z value is not a valid number. Returning None.")
|
300
|
+
return None
|
301
|
+
|
302
|
+
vertex = None
|
303
|
+
try:
|
304
|
+
vertex = topologic.Vertex.ByCoordinates(x, y, z)
|
305
|
+
except:
|
306
|
+
vertex = None
|
307
|
+
print("Vertex.ByCoordinates - Error: Could not create a topologic vertex. Returning None.")
|
308
|
+
return vertex
|
309
|
+
|
310
|
+
@staticmethod
|
311
|
+
def Centroid(vertices):
|
312
|
+
"""
|
313
|
+
Returns the centroid of the input list of vertices.
|
314
|
+
|
315
|
+
Parameters
|
316
|
+
-----------
|
317
|
+
vertices : list
|
318
|
+
The input list of vertices
|
319
|
+
|
320
|
+
Return
|
321
|
+
----------
|
322
|
+
topologic.Vertex
|
323
|
+
The computed centroid of the input list of vertices
|
324
|
+
"""
|
325
|
+
|
326
|
+
if not isinstance(vertices, list):
|
327
|
+
print("Vertex.Centroid - Error: The input vertices parameter is not a valid list. Returning None.")
|
328
|
+
return None
|
329
|
+
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
330
|
+
if len(vertices) < 1:
|
331
|
+
print("Vertex.Centroid - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
|
332
|
+
return None
|
333
|
+
if len(vertices) == 1:
|
334
|
+
return vertices[0]
|
335
|
+
cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
|
336
|
+
cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)
|
337
|
+
cz = sum(Vertex.Z(v) for v in vertices) / len(vertices)
|
338
|
+
return Vertex.ByCoordinates(cx, cy, cz)
|
339
|
+
|
340
|
+
@staticmethod
|
341
|
+
def Clockwise2D(vertices):
|
342
|
+
"""
|
343
|
+
Sorts the input list of vertices in a clockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.
|
344
|
+
|
345
|
+
Parameters
|
346
|
+
-----------
|
347
|
+
vertices : list
|
348
|
+
The input list of vertices
|
349
|
+
|
350
|
+
Return
|
351
|
+
-----------
|
352
|
+
list
|
353
|
+
The input list of vertices sorted in a counter clockwise fashion
|
354
|
+
|
355
|
+
"""
|
356
|
+
return list(reversed(Vertex.CounterClockwise2D(vertices)))
|
357
|
+
|
358
|
+
@staticmethod
|
359
|
+
def Coordinates(vertex: topologic.Vertex, outputType: str = "xyz", mantissa: int = 6) -> list:
|
360
|
+
"""
|
361
|
+
Returns the coordinates of the input vertex.
|
362
|
+
|
363
|
+
Parameters
|
364
|
+
----------
|
365
|
+
vertex : topologic.Vertex
|
366
|
+
The input vertex.
|
367
|
+
outputType : string, optional
|
368
|
+
The desired output type. Could be any permutation or substring of "xyz" or the string "matrix". The default is "xyz". The input is case insensitive and the coordinates will be returned in the specified order.
|
369
|
+
mantissa : int , optional
|
370
|
+
The desired length of the mantissa. The default is 6.
|
371
|
+
|
372
|
+
Returns
|
373
|
+
-------
|
374
|
+
list
|
375
|
+
The coordinates of the input vertex.
|
376
|
+
|
377
|
+
"""
|
378
|
+
if not isinstance(vertex, topologic.Vertex):
|
379
|
+
return None
|
380
|
+
x = round(vertex.X(), mantissa)
|
381
|
+
y = round(vertex.Y(), mantissa)
|
382
|
+
z = round(vertex.Z(), mantissa)
|
383
|
+
matrix = [[1, 0, 0, x],
|
384
|
+
[0, 1, 0, y],
|
385
|
+
[0, 0, 1, z],
|
386
|
+
[0, 0, 0, 1]]
|
387
|
+
output = []
|
388
|
+
outputType = outputType.lower()
|
389
|
+
if outputType == "matrix":
|
390
|
+
return matrix
|
391
|
+
else:
|
392
|
+
outputType = list(outputType)
|
393
|
+
for axis in outputType:
|
394
|
+
if axis == "x":
|
395
|
+
output.append(x)
|
396
|
+
elif axis == "y":
|
397
|
+
output.append(y)
|
398
|
+
elif axis == "z":
|
399
|
+
output.append(z)
|
400
|
+
return output
|
401
|
+
|
402
|
+
@staticmethod
|
403
|
+
def CounterClockwise2D(vertices):
|
404
|
+
"""
|
405
|
+
Sorts the input list of vertices in a counterclockwise fashion. This method assumes that the vertices are on the XY plane. The Z coordinate is ignored.
|
406
|
+
|
407
|
+
Parameters
|
408
|
+
-----------
|
409
|
+
vertices : list
|
410
|
+
The input list of vertices
|
411
|
+
|
412
|
+
Return
|
413
|
+
-----------
|
414
|
+
list
|
415
|
+
The input list of vertices sorted in a counter clockwise fashion
|
416
|
+
|
417
|
+
"""
|
418
|
+
import math
|
419
|
+
# find the centroid of the points
|
420
|
+
cx = sum(Vertex.X(v) for v in vertices) / len(vertices)
|
421
|
+
cy = sum(Vertex.Y(v) for v in vertices) / len(vertices)
|
422
|
+
|
423
|
+
# sort the points based on their angle with respect to the centroid
|
424
|
+
vertices.sort(key=lambda v: (math.atan2(Vertex.Y(v) - cy, Vertex.X(v) - cx) + 2 * math.pi) % (2 * math.pi))
|
425
|
+
return vertices
|
426
|
+
|
427
|
+
@staticmethod
|
428
|
+
def Degree(vertex: topologic.Vertex, hostTopology: topologic.Topology, topologyType: str = "edge"):
|
429
|
+
"""
|
430
|
+
Returns the vertex degree (the number of super topologies connected to it). See https://en.wikipedia.org/wiki/Degree_(graph_theory).
|
431
|
+
|
432
|
+
Parameters
|
433
|
+
----------
|
434
|
+
vertex : topologic.Vertex
|
435
|
+
The input vertex.
|
436
|
+
hostTopology : topologic.Topology
|
437
|
+
The input host topology in which to search for the connected super topologies.
|
438
|
+
topologyType : str , optional
|
439
|
+
The topology type to search for. This can be any of "edge", "wire", "face", "shell", "cell", "cellcomplex", "cluster". It is case insensitive. If set to None, the immediate supertopology type is searched for. The default is None.
|
440
|
+
|
441
|
+
Returns
|
442
|
+
-------
|
443
|
+
int
|
444
|
+
The number of super topologies connected to this vertex
|
445
|
+
|
446
|
+
"""
|
447
|
+
from topologicpy.Topology import Topology
|
448
|
+
|
449
|
+
if not isinstance(vertex, topologic.Vertex):
|
450
|
+
print("Vertex.Degree - Error: The input vertex parameter is not a valid topologic vertex. Returning None.")
|
451
|
+
if not isinstance(hostTopology, topologic.Topology):
|
452
|
+
print("Vertex.Degree - Error: The input hostTopology parameter is not a valid topologic topology. Returning None.")
|
453
|
+
superTopologies = Topology.SuperTopologies(topology=vertex, hostTopology=hostTopology, topologyType=topologyType)
|
454
|
+
return len(superTopologies)
|
455
|
+
|
456
|
+
|
457
|
+
@staticmethod
|
458
|
+
def Distance(vertex: topologic.Vertex, topology: topologic.Topology, includeCentroid: bool =True,
|
459
|
+
mantissa: int = 6) -> float:
|
460
|
+
"""
|
461
|
+
Returns the distance between the input vertex and the input topology. This method returns the distance to the closest sub-topology in the input topology, optionally including its centroid.
|
462
|
+
|
463
|
+
Parameters
|
464
|
+
----------
|
465
|
+
vertex : topologic.Vertex
|
466
|
+
The input vertex.
|
467
|
+
topology : topologic.Topology
|
468
|
+
The input topology.
|
469
|
+
includeCentroid : bool
|
470
|
+
If set to True, the centroid of the input topology will be considered in finding the nearest subTopology to the input vertex. The default is True.
|
471
|
+
mantissa : int , optional
|
472
|
+
The desired length of the mantissa. The default is 6.
|
473
|
+
|
474
|
+
Returns
|
475
|
+
-------
|
476
|
+
float
|
477
|
+
The distance between the input vertex and the input topology.
|
478
|
+
|
479
|
+
"""
|
480
|
+
from topologicpy.Edge import Edge
|
481
|
+
from topologicpy.Face import Face
|
482
|
+
import math
|
483
|
+
|
484
|
+
def distance_point_to_point(point1, point2):
|
485
|
+
# Convert input points to NumPy arrays
|
486
|
+
point1 = np.array(point1)
|
487
|
+
point2 = np.array(point2)
|
488
|
+
|
489
|
+
# Calculate the Euclidean distance
|
490
|
+
distance = np.linalg.norm(point1 - point2)
|
491
|
+
|
492
|
+
return distance
|
493
|
+
|
494
|
+
def distance_point_to_line(point, line_start, line_end):
|
495
|
+
# Convert input points to NumPy arrays for vector operations
|
496
|
+
point = np.array(point)
|
497
|
+
line_start = np.array(line_start)
|
498
|
+
line_end = np.array(line_end)
|
499
|
+
|
500
|
+
# Calculate the direction vector of the edge
|
501
|
+
line_direction = line_end - line_start
|
502
|
+
|
503
|
+
# Vector from the edge's starting point to the point
|
504
|
+
point_to_start = point - line_start
|
505
|
+
|
506
|
+
# Calculate the parameter 't' where the projection of the point onto the edge occurs
|
507
|
+
if np.dot(line_direction, line_direction) == 0:
|
508
|
+
t = 0
|
509
|
+
else:
|
510
|
+
t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
|
511
|
+
|
512
|
+
# Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
|
513
|
+
if t < 0:
|
514
|
+
return np.linalg.norm(point - line_start)
|
515
|
+
elif t > 1:
|
516
|
+
return np.linalg.norm(point - line_end)
|
517
|
+
|
518
|
+
# Calculate the closest point on the edge to the given point
|
519
|
+
closest_point = line_start + t * line_direction
|
520
|
+
|
521
|
+
# Calculate the distance between the closest point and the given point
|
522
|
+
distance = np.linalg.norm(point - closest_point)
|
523
|
+
|
524
|
+
return distance
|
525
|
+
|
526
|
+
def distance_to_vertex(vertexA, vertexB):
|
527
|
+
a = (Vertex.X(vertexA), Vertex.Y(vertexA), Vertex.Z(vertexA))
|
528
|
+
b = (Vertex.X(vertexB), Vertex.Y(vertexB), Vertex.Z(vertexB))
|
529
|
+
return distance_point_to_point(a, b)
|
530
|
+
|
531
|
+
def distance_to_edge(vertex, edge):
|
532
|
+
a = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
|
533
|
+
sv = Edge.StartVertex(edge)
|
534
|
+
ev = Edge.EndVertex(edge)
|
535
|
+
svp = (Vertex.X(sv), Vertex.Y(sv), Vertex.Z(sv))
|
536
|
+
evp = (Vertex.X(ev), Vertex.Y(ev), Vertex.Z(ev))
|
537
|
+
return distance_point_to_line(a,svp, evp)
|
538
|
+
|
539
|
+
def distance_to_face(vertex, face, includeCentroid):
|
540
|
+
v_proj = Vertex.Project(vertex, face, mantissa=mantissa)
|
541
|
+
if not Vertex.IsInternal(v_proj, face):
|
542
|
+
vertices = Topology.Vertices(topology)
|
543
|
+
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
544
|
+
edges = Topology.Edges(topology)
|
545
|
+
distances += [distance_to_edge(vertex, e) for e in edges]
|
546
|
+
if includeCentroid:
|
547
|
+
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
548
|
+
return min(distances)
|
549
|
+
dic = Face.PlaneEquation(face)
|
550
|
+
a = dic["a"]
|
551
|
+
b = dic["b"]
|
552
|
+
c = dic["c"]
|
553
|
+
d = dic["d"]
|
554
|
+
x1, y1, z1 = Vertex.Coordinates(vertex)
|
555
|
+
d = abs((a * x1 + b * y1 + c * z1 + d))
|
556
|
+
e = (math.sqrt(a * a + b * b + c * c))
|
557
|
+
if e == 0:
|
558
|
+
return 0
|
559
|
+
return d/e
|
560
|
+
if not isinstance(vertex, topologic.Vertex) or not isinstance(topology, topologic.Topology):
|
561
|
+
return None
|
562
|
+
if isinstance(topology, topologic.Vertex):
|
563
|
+
return round(distance_to_vertex(vertex,topology), mantissa)
|
564
|
+
elif isinstance(topology, topologic.Edge):
|
565
|
+
return round(distance_to_edge(vertex,topology), mantissa)
|
566
|
+
elif isinstance(topology, topologic.Wire):
|
567
|
+
vertices = Topology.Vertices(topology)
|
568
|
+
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
569
|
+
edges = Topology.Edges(topology)
|
570
|
+
distances += [distance_to_edge(vertex, e) for e in edges]
|
571
|
+
if includeCentroid:
|
572
|
+
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
573
|
+
return round(min(distances), mantissa)
|
574
|
+
elif isinstance(topology, topologic.Face):
|
575
|
+
vertices = Topology.Vertices(topology)
|
576
|
+
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
577
|
+
edges = Topology.Edges(topology)
|
578
|
+
distances += [distance_to_edge(vertex, e) for e in edges]
|
579
|
+
distances.append(distance_to_face(vertex,topology, includeCentroid))
|
580
|
+
if includeCentroid:
|
581
|
+
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
582
|
+
return round(min(distances), mantissa)
|
583
|
+
elif isinstance(topology, topologic.Shell) or isinstance(topology, topologic.Cell) or isinstance(topology, topologic.CellComplex) or isinstance(topology, topologic.Cluster):
|
584
|
+
vertices = Topology.Vertices(topology)
|
585
|
+
distances = [distance_to_vertex(vertex, v) for v in vertices]
|
586
|
+
edges = Topology.Edges(topology)
|
587
|
+
distances += [distance_to_edge(vertex, e) for e in edges]
|
588
|
+
faces = Topology.Faces(topology)
|
589
|
+
distances += [distance_to_face(vertex, f, includeCentroid) for f in faces]
|
590
|
+
if includeCentroid:
|
591
|
+
distances.append(distance_to_vertex(vertex, Topology.Centroid(topology)))
|
592
|
+
return round(min(distances), mantissa)
|
593
|
+
else:
|
594
|
+
print("Vertex.Distance - Error: Could not recognize the input topology. Returning None.")
|
595
|
+
return None
|
596
|
+
|
597
|
+
@staticmethod
|
598
|
+
def EnclosingCell(vertex: topologic.Vertex, topology: topologic.Topology, exclusive: bool = True, tolerance: float = 0.0001) -> list:
|
599
|
+
"""
|
600
|
+
Returns the list of Cells found in the input topology that enclose the input vertex.
|
601
|
+
|
602
|
+
Parameters
|
603
|
+
----------
|
604
|
+
vertex : topologic.Vertex
|
605
|
+
The input vertex.
|
606
|
+
topology : topologic.Topology
|
607
|
+
The input topology.
|
608
|
+
exclusive : bool , optional
|
609
|
+
If set to True, return only the first found enclosing cell. The default is True.
|
610
|
+
tolerance : float , optional
|
611
|
+
The tolerance for computing if the input vertex is enclosed in a cell. The default is 0.0001.
|
612
|
+
|
613
|
+
Returns
|
614
|
+
-------
|
615
|
+
list
|
616
|
+
The list of enclosing cells.
|
617
|
+
|
618
|
+
"""
|
619
|
+
|
620
|
+
def boundingBox(cell):
|
621
|
+
vertices = []
|
622
|
+
_ = cell.Vertices(None, vertices)
|
623
|
+
x = []
|
624
|
+
y = []
|
625
|
+
z = []
|
626
|
+
for aVertex in vertices:
|
627
|
+
x.append(aVertex.X())
|
628
|
+
y.append(aVertex.Y())
|
629
|
+
z.append(aVertex.Z())
|
630
|
+
return ([min(x), min(y), min(z), max(x), max(y), max(z)])
|
631
|
+
|
632
|
+
if isinstance(topology, topologic.Cell):
|
633
|
+
cells = [topology]
|
634
|
+
elif isinstance(topology, topologic.Cluster) or isinstance(topology, topologic.CellComplex):
|
635
|
+
cells = []
|
636
|
+
_ = topology.Cells(None, cells)
|
637
|
+
else:
|
638
|
+
return None
|
639
|
+
if len(cells) < 1:
|
640
|
+
return None
|
641
|
+
enclosingCells = []
|
642
|
+
for i in range(len(cells)):
|
643
|
+
bbox = boundingBox(cells[i])
|
644
|
+
if ((vertex.X() < bbox[0]) or (vertex.Y() < bbox[1]) or (vertex.Z() < bbox[2]) or (vertex.X() > bbox[3]) or (vertex.Y() > bbox[4]) or (vertex.Z() > bbox[5])) == False:
|
645
|
+
if topologic.CellUtility.Contains(cells[i], vertex, tolerance) == 0:
|
646
|
+
if exclusive:
|
647
|
+
return([cells[i]])
|
648
|
+
else:
|
649
|
+
enclosingCells.append(cells[i])
|
650
|
+
return enclosingCells
|
651
|
+
|
652
|
+
@staticmethod
|
653
|
+
def Fuse(vertices: list, mantissa: int = 6, tolerance: float = 0.0001):
|
654
|
+
"""
|
655
|
+
Returns a list of vertices where vertices within a specified tolerance distance are fused while retaining duplicates, ensuring that vertices with nearly identical coordinates are replaced by a single shared coordinate.
|
656
|
+
|
657
|
+
Parameters
|
658
|
+
----------
|
659
|
+
vertices : list
|
660
|
+
The input list of topologic vertices.
|
661
|
+
mantissa : int , optional
|
662
|
+
The desired length of the mantissa for retrieving vertex coordinates. The default is 6.
|
663
|
+
tolerance : float , optional
|
664
|
+
The desired tolerance for computing if vertices need to be fused. Any vertices that are closer to each other than this tolerance will be fused. The default is 0.0001.
|
665
|
+
|
666
|
+
Returns
|
667
|
+
-------
|
668
|
+
list
|
669
|
+
The list of fused vertices. This list contains the same number of vertices and in the same order as the input list of vertices. However, the coordinates
|
670
|
+
of these vertices have now been modified so that they are exactly the same with other vertices that are within the tolerance distance.
|
671
|
+
"""
|
672
|
+
|
673
|
+
import numpy as np
|
674
|
+
|
675
|
+
def fuse_vertices(vertices, tolerance):
|
676
|
+
fused_vertices = []
|
677
|
+
merged_indices = {}
|
678
|
+
|
679
|
+
for idx, vertex in enumerate(vertices):
|
680
|
+
if idx in merged_indices:
|
681
|
+
fused_vertices.append(fused_vertices[merged_indices[idx]])
|
682
|
+
continue
|
683
|
+
|
684
|
+
merged_indices[idx] = len(fused_vertices)
|
685
|
+
fused_vertex = vertex
|
686
|
+
for i in range(idx + 1, len(vertices)):
|
687
|
+
if i in merged_indices:
|
688
|
+
continue
|
689
|
+
|
690
|
+
other_vertex = vertices[i]
|
691
|
+
distance = np.linalg.norm(np.array(vertex) - np.array(other_vertex))
|
692
|
+
if distance < tolerance:
|
693
|
+
# Choose the coordinate with the least amount of decimal points
|
694
|
+
if count_decimal_points(other_vertex) < count_decimal_points(fused_vertex):
|
695
|
+
fused_vertex = other_vertex
|
696
|
+
|
697
|
+
merged_indices[i] = len(fused_vertices)
|
698
|
+
|
699
|
+
fused_vertices.append(fused_vertex)
|
700
|
+
|
701
|
+
return fused_vertices
|
702
|
+
def count_decimal_points(vertex):
|
703
|
+
# Count the number of decimal points in the coordinates
|
704
|
+
decimals_list = []
|
705
|
+
for coord in vertex:
|
706
|
+
coord_str = str(coord)
|
707
|
+
if '.' in coord_str:
|
708
|
+
decimals_list.append(len(coord_str.split('.')[1]))
|
709
|
+
elif 'e' in coord_str:
|
710
|
+
decimals_list.append(int(coord_str.split('e')[1].replace('-','')))
|
711
|
+
return max(decimals_list)
|
712
|
+
|
713
|
+
|
714
|
+
if not isinstance(vertices, list):
|
715
|
+
print("Vertex.Fuse - Error: The input vertices parameter is not a valid list. Returning None.")
|
716
|
+
return None
|
717
|
+
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
718
|
+
if len(vertices) == 0:
|
719
|
+
print("Vertex.Fuse - Error: The input vertices parameter does not contain any valid topologic vertices. Returning None.")
|
720
|
+
return None
|
721
|
+
|
722
|
+
vertices = [(Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa), Vertex.Z(v, mantissa=mantissa)) for v in vertices]
|
723
|
+
fused_vertices = fuse_vertices(vertices, tolerance)
|
724
|
+
return_vertices = [Vertex.ByCoordinates(list(coord)) for coord in fused_vertices]
|
725
|
+
return return_vertices
|
726
|
+
|
727
|
+
@staticmethod
|
728
|
+
def Index(vertex: topologic.Vertex, vertices: list, strict: bool = False, tolerance: float = 0.0001) -> int:
|
729
|
+
"""
|
730
|
+
Returns index of the input vertex in the input list of vertices
|
731
|
+
|
732
|
+
Parameters
|
733
|
+
----------
|
734
|
+
vertex : topologic.Vertex
|
735
|
+
The input vertex.
|
736
|
+
vertices : list
|
737
|
+
The input list of vertices.
|
738
|
+
strict : bool , optional
|
739
|
+
If set to True, the vertex must be strictly identical to the one found in the list. Otherwise, a distance comparison is used. The default is False.
|
740
|
+
tolerance : float , optional
|
741
|
+
The tolerance for computing if the input vertex is identical to a vertex from the list. The default is 0.0001.
|
742
|
+
|
743
|
+
Returns
|
744
|
+
-------
|
745
|
+
int
|
746
|
+
The index of the input vertex in the input list of vertices.
|
747
|
+
|
748
|
+
"""
|
749
|
+
from topologicpy.Topology import Topology
|
750
|
+
if not isinstance(vertex, topologic.Vertex):
|
751
|
+
return None
|
752
|
+
if not isinstance(vertices, list):
|
753
|
+
return None
|
754
|
+
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
755
|
+
if len(vertices) == 0:
|
756
|
+
return None
|
757
|
+
for i in range(len(vertices)):
|
758
|
+
if strict:
|
759
|
+
if Topology.IsSame(vertex, vertices[i]):
|
760
|
+
return i
|
761
|
+
else:
|
762
|
+
d = Vertex.Distance(vertex, vertices[i])
|
763
|
+
if d < tolerance:
|
764
|
+
return i
|
765
|
+
return None
|
766
|
+
|
767
|
+
@staticmethod
|
768
|
+
def InterpolateValue(vertex, vertices, n=3, key="intensity", tolerance=0.0001):
|
769
|
+
"""
|
770
|
+
Interpolates the value of the input vertex based on the values of the *n* nearest vertices.
|
771
|
+
|
772
|
+
Parameters
|
773
|
+
----------
|
774
|
+
vertex : topologic.Vertex
|
775
|
+
The input vertex.
|
776
|
+
vertices : list
|
777
|
+
The input list of vertices.
|
778
|
+
n : int , optional
|
779
|
+
The maximum number of nearest vertices to consider. The default is 3.
|
780
|
+
key : str , optional
|
781
|
+
The key that holds the value to be interpolated in the dictionaries of the vertices. The default is "intensity".
|
782
|
+
tolerance : float , optional
|
783
|
+
The tolerance for computing if the input vertex is coincident with another vertex in the input list of vertices. The default is 0.0001.
|
784
|
+
|
785
|
+
Returns
|
786
|
+
-------
|
787
|
+
topologic.vertex
|
788
|
+
The input vertex with the interpolated value stored in its dictionary at the key specified by the input key. Other keys and values in the dictionary are preserved.
|
789
|
+
|
790
|
+
"""
|
791
|
+
|
792
|
+
def interpolate_value(point, data_points, n, tolerance=0.0001):
|
793
|
+
"""
|
794
|
+
Interpolates the value associated with a point in 3D by averaging the values of the n nearest points.
|
795
|
+
The influence of the adjacent points is inversely proportional to their distance from the input point.
|
796
|
+
|
797
|
+
Args:
|
798
|
+
data_points (list): A list of tuples, each representing a data point in 3D space as (x, y, z, value).
|
799
|
+
The 'value' represents the value associated with that data point.
|
800
|
+
point (tuple): A tuple representing the point in 3D space as (x, y, z) for which we want to interpolate a value.
|
801
|
+
n (int): The number of nearest points to consider for interpolation.
|
802
|
+
|
803
|
+
Returns:
|
804
|
+
The interpolated value for the input point.
|
805
|
+
"""
|
806
|
+
# Calculate the distances between the input point and all data points
|
807
|
+
distances = [(distance(p[:3], point), p[3]) for p in data_points]
|
808
|
+
|
809
|
+
# Sort the distances in ascending order
|
810
|
+
sorted_distances = sorted(distances, key=lambda x: x[0])
|
811
|
+
|
812
|
+
# Take the n nearest points
|
813
|
+
nearest_points = sorted_distances[:n]
|
814
|
+
|
815
|
+
n_p = nearest_points[0]
|
816
|
+
n_d = n_p[0]
|
817
|
+
if n_d < tolerance:
|
818
|
+
return n_p[1]
|
819
|
+
|
820
|
+
# Calculate the weights for each nearest point based on inverse distance
|
821
|
+
|
822
|
+
weights = [(1/d[0], d[1]) for d in nearest_points]
|
823
|
+
|
824
|
+
# Normalize the weights so they sum to 1
|
825
|
+
total_weight = sum(w[0] for w in weights)
|
826
|
+
normalized_weights = [(w[0]/total_weight, w[1]) for w in weights]
|
827
|
+
|
828
|
+
# Interpolate the value as the weighted average of the nearest points
|
829
|
+
interpolated_value = sum(w[0]*w[1] for w in normalized_weights)
|
830
|
+
|
831
|
+
return interpolated_value
|
832
|
+
|
833
|
+
def distance(point1, point2):
|
834
|
+
"""
|
835
|
+
Calculates the Euclidean distance between two points in 3D space.
|
836
|
+
|
837
|
+
Args:
|
838
|
+
point1 (tuple): A tuple representing a point in 3D space as (x, y, z).
|
839
|
+
point2 (tuple): A tuple representing a point in 3D space as (x, y, z).
|
840
|
+
|
841
|
+
Returns:
|
842
|
+
The Euclidean distance between the two points.
|
843
|
+
"""
|
844
|
+
return ((point1[0]-point2[0])**2 + (point1[1]-point2[1])**2 + (point1[2]-point2[2])**2)**0.5
|
845
|
+
|
846
|
+
from topologicpy.Topology import Topology
|
847
|
+
from topologicpy.Dictionary import Dictionary
|
848
|
+
|
849
|
+
if not isinstance(vertex, topologic.Vertex):
|
850
|
+
return None
|
851
|
+
if not isinstance(vertices, list):
|
852
|
+
return None
|
853
|
+
|
854
|
+
vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
|
855
|
+
if len(vertices) == 0:
|
856
|
+
return None
|
857
|
+
|
858
|
+
point = (Vertex.X(vertex), Vertex.Y(vertex), Vertex.Z(vertex))
|
859
|
+
data_points = []
|
860
|
+
for v in vertices:
|
861
|
+
d = Topology.Dictionary(v)
|
862
|
+
value = Dictionary.ValueAtKey(d, key)
|
863
|
+
if not value == None:
|
864
|
+
if type(value) == int or type(value) == float:
|
865
|
+
data_points.append((Vertex.X(v), Vertex.Y(v), Vertex.Z(v), value))
|
866
|
+
if len(data_points) == 0:
|
867
|
+
return None
|
868
|
+
if n > len(data_points):
|
869
|
+
n = len(data_points)
|
870
|
+
value = interpolate_value(point, data_points, n, tolerance=0.0001)
|
871
|
+
d = Topology.Dictionary(vertex)
|
872
|
+
d = Dictionary.SetValueAtKey(d, key, value)
|
873
|
+
vertex = Topology.SetDictionary(vertex, d)
|
874
|
+
return vertex
|
875
|
+
|
876
|
+
@staticmethod
|
877
|
+
def IsCoincident(vertexA: topologic.Vertex, vertexB: topologic.Vertex, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
878
|
+
"""
|
879
|
+
Returns True if the input vertexA is coincident with the input vertexB. Returns False otherwise.
|
880
|
+
|
881
|
+
Parameters
|
882
|
+
----------
|
883
|
+
vertexA : topologic.Vertex
|
884
|
+
The first input vertex.
|
885
|
+
vertexB : topologic.Vertex
|
886
|
+
The second input vertex.
|
887
|
+
tolerance : float , optional
|
888
|
+
The tolerance for computing if the input vertexA is coincident with the input vertexB. The default is 0.0001.
|
889
|
+
|
890
|
+
Returns
|
891
|
+
-------
|
892
|
+
bool
|
893
|
+
True if the input vertexA is coincident with the input vertexB. False otherwise.
|
894
|
+
|
895
|
+
"""
|
896
|
+
if not isinstance(vertexA, topologic.Vertex):
|
897
|
+
if not silent:
|
898
|
+
print("Vertex.IsCoincident - Error: The input vertexA parameter is not a valid vertex. Returning None.")
|
899
|
+
return None
|
900
|
+
if not isinstance(vertexB, topologic.Vertex):
|
901
|
+
if not silent:
|
902
|
+
print("Vertex.IsICoincident - Error: The input vertexB parameter is not a valid vertex. Returning None.")
|
903
|
+
return None
|
904
|
+
return Vertex.IsInternal(vertexA, vertexB, tolerance=tolerance, silent=silent)
|
905
|
+
|
906
|
+
@staticmethod
|
907
|
+
def IsExternal(vertex: topologic.Vertex, topology: topologic.Topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
908
|
+
"""
|
909
|
+
Returns True if the input vertex is external to the input topology. Returns False otherwise.
|
910
|
+
|
911
|
+
Parameters
|
912
|
+
----------
|
913
|
+
vertex : topologic.Vertex
|
914
|
+
The input vertex.
|
915
|
+
topology : topologic.Topology
|
916
|
+
The input topology.
|
917
|
+
tolerance : float , optional
|
918
|
+
The tolerance for computing if the input vertex is external to the input topology. The default is 0.0001.
|
919
|
+
silent : bool , optional
|
920
|
+
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
|
921
|
+
|
922
|
+
Returns
|
923
|
+
-------
|
924
|
+
bool
|
925
|
+
True if the input vertex is external to the input topology. False otherwise.
|
926
|
+
|
927
|
+
"""
|
928
|
+
|
929
|
+
if not isinstance(vertex, topologic.Vertex):
|
930
|
+
if not silent:
|
931
|
+
print("Vertex.IsExternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
|
932
|
+
return None
|
933
|
+
if not isinstance(topology, topologic.Topology):
|
934
|
+
if not silent:
|
935
|
+
print("Vertex.IsExternal - Error: The input topology parameter is not a valid topology. Returning None.")
|
936
|
+
return None
|
937
|
+
return not (Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent) or Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent))
|
938
|
+
|
939
|
+
@staticmethod
|
940
|
+
def IsInternal(vertex: topologic.Vertex, topology: topologic.Topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
941
|
+
"""
|
942
|
+
Returns True if the input vertex is inside the input topology. Returns False otherwise.
|
943
|
+
|
944
|
+
Parameters
|
945
|
+
----------
|
946
|
+
vertex : topologic.Vertex
|
947
|
+
The input vertex.
|
948
|
+
topology : topologic.Topology
|
949
|
+
The input topology.
|
950
|
+
tolerance : float , optional
|
951
|
+
The tolerance for computing if the input vertex is internal to the input topology. The default is 0.0001.
|
952
|
+
silent : bool , optional
|
953
|
+
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
|
954
|
+
|
955
|
+
Returns
|
956
|
+
-------
|
957
|
+
bool
|
958
|
+
True if the input vertex is internal to the input topology. False otherwise.
|
959
|
+
|
960
|
+
"""
|
961
|
+
from topologicpy.Edge import Edge
|
962
|
+
from topologicpy.Wire import Wire
|
963
|
+
from topologicpy.Face import Face
|
964
|
+
from topologicpy.Shell import Shell
|
965
|
+
from topologicpy.CellComplex import CellComplex
|
966
|
+
from topologicpy.Cluster import Cluster
|
967
|
+
from topologicpy.Topology import Topology
|
968
|
+
if not isinstance(vertex, topologic.Vertex):
|
969
|
+
if not silent:
|
970
|
+
print("Vertex.IsInternal - Error: The input vertex parameter is not a valid vertex. Returning None.")
|
971
|
+
return None
|
972
|
+
if not isinstance(topology, topologic.Topology):
|
973
|
+
if not silent:
|
974
|
+
print("Vertex.IsInternal - Error: The input topology parameter is not a valid topology. Returning None.")
|
975
|
+
return None
|
976
|
+
|
977
|
+
if isinstance(topology, topologic.Vertex):
|
978
|
+
return Vertex.Distance(vertex, topology) < tolerance
|
979
|
+
elif isinstance(topology, topologic.Edge):
|
980
|
+
try:
|
981
|
+
parameter = topologic.EdgeUtility.ParameterAtPoint(topology, vertex)
|
982
|
+
except:
|
983
|
+
parameter = 400 #aribtrary large number greater than 1
|
984
|
+
return 0 <= parameter <= 1
|
985
|
+
elif isinstance(topology, topologic.Wire):
|
986
|
+
vertices = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) > 1]
|
987
|
+
edges = Wire.Edges(topology)
|
988
|
+
sub_list = vertices + edges
|
989
|
+
for sub in sub_list:
|
990
|
+
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
991
|
+
return True
|
992
|
+
return False
|
993
|
+
elif isinstance(topology, topologic.Face):
|
994
|
+
# Test the distance first
|
995
|
+
if Vertex.PerpendicularDistance(vertex, topology) > tolerance:
|
996
|
+
return False
|
997
|
+
if Vertex.IsPeripheral(vertex, topology):
|
998
|
+
return False
|
999
|
+
normal = Face.Normal(topology)
|
1000
|
+
proj_v = Vertex.Project(vertex, topology)
|
1001
|
+
v1 = Topology.TranslateByDirectionDistance(proj_v, normal, 1)
|
1002
|
+
v2 = Topology.TranslateByDirectionDistance(proj_v, normal, -1)
|
1003
|
+
edge = Edge.ByVertices(v1, v2)
|
1004
|
+
intersect = edge.Intersect(topology)
|
1005
|
+
if intersect == None:
|
1006
|
+
return False
|
1007
|
+
return True
|
1008
|
+
elif isinstance(topology, topologic.Shell):
|
1009
|
+
if Vertex.IsPeripheral(vertex, topology, tolerance=tolerance, silent=silent):
|
1010
|
+
return False
|
1011
|
+
else:
|
1012
|
+
edges = Topology.Edges(topology)
|
1013
|
+
for edge in edges:
|
1014
|
+
if Vertex.IsInternal(vertex, edge, tolerance=tolerance, silent=silent):
|
1015
|
+
return True
|
1016
|
+
faces = Topology.Faces(topology)
|
1017
|
+
for face in faces:
|
1018
|
+
if Vertex.IsInternal(vertex, face, tolerance=tolerance, silent=silent):
|
1019
|
+
return True
|
1020
|
+
return False
|
1021
|
+
elif isinstance(topology, topologic.Cell):
|
1022
|
+
return topologic.CellUtility.Contains(topology, vertex, tolerance) == 0
|
1023
|
+
elif isinstance(topology, topologic.CellComplex):
|
1024
|
+
ext_boundary = CellComplex.ExternalBoundary(topology)
|
1025
|
+
return Vertex.IsInternal(vertex, ext_boundary, tolerance=tolerance, silent=silent)
|
1026
|
+
elif isinstance(topology, topologic.Cluster):
|
1027
|
+
sub_list = Cluster.FreeTopologies(topology)
|
1028
|
+
for sub in sub_list:
|
1029
|
+
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1030
|
+
return True
|
1031
|
+
return False
|
1032
|
+
return False
|
1033
|
+
|
1034
|
+
|
1035
|
+
@staticmethod
|
1036
|
+
def IsPeripheral(vertex: topologic.Vertex, topology: topologic.Topology, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
1037
|
+
"""
|
1038
|
+
Returns True if the input vertex is peripheral to the input topology. Returns False otherwise.
|
1039
|
+
A vertex is said to be peripheral to the input topology if:
|
1040
|
+
01. Vertex: If it is internal to it (i.e. coincident with it).
|
1041
|
+
02. Edge: If it is internal to its start or end vertices.
|
1042
|
+
03. Manifold open wire: If it is internal to its start or end vertices.
|
1043
|
+
04. Manifold closed wire: If it is internal to any of its vertices.
|
1044
|
+
05. Non-manifold wire: If it is internal to any of its vertices that has a vertex degree of 1.
|
1045
|
+
06. Face: If it is internal to any of its edges or vertices.
|
1046
|
+
07. Shell: If it is internal to external boundary
|
1047
|
+
08. Cell: If it is internal to any of its faces, edges, or vertices.
|
1048
|
+
09. CellComplex: If it is peripheral to its external boundary.
|
1049
|
+
10. Cluster: If it is peripheral to any of its free topologies. (See Cluster.FreeTopologies)
|
1050
|
+
|
1051
|
+
Parameters
|
1052
|
+
----------
|
1053
|
+
vertex : topologic.Vertex
|
1054
|
+
The input vertex.
|
1055
|
+
topology : topologic.Topology
|
1056
|
+
The input topology.
|
1057
|
+
tolerance : float , optional
|
1058
|
+
The tolerance for computing if the input vertex is peripheral to the input topology. The default is 0.0001.
|
1059
|
+
silent : bool , optional
|
1060
|
+
If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
|
1061
|
+
|
1062
|
+
Returns
|
1063
|
+
-------
|
1064
|
+
bool
|
1065
|
+
True if the input vertex is peripheral to the input topology. False otherwise.
|
1066
|
+
|
1067
|
+
"""
|
1068
|
+
from topologicpy.Edge import Edge
|
1069
|
+
from topologicpy.Wire import Wire
|
1070
|
+
from topologicpy.Face import Face
|
1071
|
+
from topologicpy.Shell import Shell
|
1072
|
+
from topologicpy.CellComplex import CellComplex
|
1073
|
+
from topologicpy.Cluster import Cluster
|
1074
|
+
from topologicpy.Topology import Topology
|
1075
|
+
|
1076
|
+
if not isinstance(vertex, topologic.Vertex):
|
1077
|
+
if not silent:
|
1078
|
+
print("Vertex.IsPeripheral - Error: The input vertex parameter is not a valid vertex. Returning None.")
|
1079
|
+
return None
|
1080
|
+
if not isinstance(topology, topologic.Topology):
|
1081
|
+
if not silent:
|
1082
|
+
print("Vertex.IsPeripheral - Error: The input topology parameter is not a valid topology. Returning None.")
|
1083
|
+
return None
|
1084
|
+
|
1085
|
+
if isinstance(topology, topologic.Vertex):
|
1086
|
+
return Vertex.IsInternal(vertex, topology, tolerance=tolerance, silent=silent)
|
1087
|
+
elif isinstance(topology, topologic.Edge):
|
1088
|
+
sv = Edge.StartVertex(topology)
|
1089
|
+
ev = Edge.EndVertex(topology)
|
1090
|
+
f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
|
1091
|
+
f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
|
1092
|
+
return f1 or f2
|
1093
|
+
elif isinstance(topology, topologic.Wire):
|
1094
|
+
if Wire.IsManifold(topology):
|
1095
|
+
if not Wire.IsClosed(topology):
|
1096
|
+
sv = Wire.StartVertex(topology)
|
1097
|
+
ev = Wire.EndVertex(topology)
|
1098
|
+
f1 = Vertex.IsInternal(vertex, sv, tolerance=tolerance, silent=silent)
|
1099
|
+
f2 = Vertex.IsInternal(vertex, ev, tolerance=tolerance, silent=silent)
|
1100
|
+
return f1 or f2
|
1101
|
+
else:
|
1102
|
+
sub_list = [v for v in Topology.Vertices(topology)]
|
1103
|
+
for sub in sub_list:
|
1104
|
+
if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
|
1105
|
+
return True
|
1106
|
+
return False
|
1107
|
+
else:
|
1108
|
+
sub_list = [v for v in Topology.Vertices(topology) if Vertex.Degree(v, topology) == 1]
|
1109
|
+
for sub in sub_list:
|
1110
|
+
if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
|
1111
|
+
return True
|
1112
|
+
return False
|
1113
|
+
elif isinstance(topology, topologic.Face):
|
1114
|
+
sub_list = Topology.Vertices(topology) + Topology.Edges(topology)
|
1115
|
+
for sub in sub_list:
|
1116
|
+
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1117
|
+
return True
|
1118
|
+
return False
|
1119
|
+
elif isinstance(topology, topologic.Shell):
|
1120
|
+
ext_boundary = Shell.ExternalBoundary(topology)
|
1121
|
+
sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary)
|
1122
|
+
for sub in sub_list:
|
1123
|
+
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1124
|
+
return True
|
1125
|
+
return False
|
1126
|
+
elif isinstance(topology, topologic.Cell):
|
1127
|
+
sub_list = Topology.Vertices(topology) + Topology.Edges(topology) + Topology.Faces(topology)
|
1128
|
+
for sub in sub_list:
|
1129
|
+
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1130
|
+
return True
|
1131
|
+
return False
|
1132
|
+
elif isinstance(topology, topologic.CellComplex):
|
1133
|
+
ext_boundary = CellComplex.ExternalBoundary(topology)
|
1134
|
+
sub_list = Topology.Vertices(ext_boundary) + Topology.Edges(ext_boundary) + Topology.Faces(ext_boundary)
|
1135
|
+
for sub in sub_list:
|
1136
|
+
if Vertex.IsInternal(vertex, sub, tolerance=tolerance, silent=silent):
|
1137
|
+
return True
|
1138
|
+
return False
|
1139
|
+
elif isinstance(topology, topologic.Cluster):
|
1140
|
+
sub_list = Cluster.FreeTopologies(topology)
|
1141
|
+
for sub in sub_list:
|
1142
|
+
if Vertex.IsPeripheral(vertex, sub, tolerance=tolerance, silent=silent):
|
1143
|
+
return True
|
1144
|
+
return False
|
1145
|
+
return False
|
1146
|
+
|
1147
|
+
@staticmethod
|
1148
|
+
def NearestVertex(vertex: topologic.Vertex, topology: topologic.Topology, useKDTree: bool = True) -> topologic.Vertex:
|
1149
|
+
"""
|
1150
|
+
Returns the vertex found in the input topology that is the nearest to the input vertex.
|
1151
|
+
|
1152
|
+
Parameters
|
1153
|
+
----------
|
1154
|
+
vertex : topologic.Vertex
|
1155
|
+
The input vertex.
|
1156
|
+
topology : topologic.Topology
|
1157
|
+
The input topology to be searched for the nearest vertex.
|
1158
|
+
useKDTree : bool , optional
|
1159
|
+
if set to True, the algorithm will use a KDTree method to search for the nearest vertex. The default is True.
|
1160
|
+
|
1161
|
+
Returns
|
1162
|
+
-------
|
1163
|
+
topologic.Vertex
|
1164
|
+
The nearest vertex.
|
1165
|
+
|
1166
|
+
"""
|
1167
|
+
def SED(a, b):
|
1168
|
+
"""Compute the squared Euclidean distance between X and Y."""
|
1169
|
+
p1 = (a.X(), a.Y(), a.Z())
|
1170
|
+
p2 = (b.X(), b.Y(), b.Z())
|
1171
|
+
return sum((i-j)**2 for i, j in zip(p1, p2))
|
1172
|
+
|
1173
|
+
BT = collections.namedtuple("BT", ["value", "left", "right"])
|
1174
|
+
BT.__doc__ = """
|
1175
|
+
A Binary Tree (BT) with a node value, and left- and
|
1176
|
+
right-subtrees.
|
1177
|
+
"""
|
1178
|
+
def firstItem(v):
|
1179
|
+
return v.X()
|
1180
|
+
def secondItem(v):
|
1181
|
+
return v.Y()
|
1182
|
+
def thirdItem(v):
|
1183
|
+
return v.Z()
|
1184
|
+
|
1185
|
+
def itemAtIndex(v, index):
|
1186
|
+
if index == 0:
|
1187
|
+
return v.X()
|
1188
|
+
elif index == 1:
|
1189
|
+
return v.Y()
|
1190
|
+
elif index == 2:
|
1191
|
+
return v.Z()
|
1192
|
+
|
1193
|
+
def sortList(vertices, index):
|
1194
|
+
if index == 0:
|
1195
|
+
vertices.sort(key=firstItem)
|
1196
|
+
elif index == 1:
|
1197
|
+
vertices.sort(key=secondItem)
|
1198
|
+
elif index == 2:
|
1199
|
+
vertices.sort(key=thirdItem)
|
1200
|
+
return vertices
|
1201
|
+
|
1202
|
+
def kdtree(topology):
|
1203
|
+
assert isinstance(topology, topologic.Topology), "Vertex.NearestVertex: The input is not a Topology."
|
1204
|
+
vertices = []
|
1205
|
+
_ = topology.Vertices(None, vertices)
|
1206
|
+
assert (len(vertices) > 0), "Vertex.NearestVertex: Could not find any vertices in the input Topology"
|
1207
|
+
|
1208
|
+
"""Construct a k-d tree from an iterable of vertices.
|
1209
|
+
|
1210
|
+
This algorithm is taken from Wikipedia. For more details,
|
1211
|
+
|
1212
|
+
> https://en.wikipedia.org/wiki/K-d_tree#Construction
|
1213
|
+
|
1214
|
+
"""
|
1215
|
+
# k = len(points[0])
|
1216
|
+
k = 3
|
1217
|
+
|
1218
|
+
def build(*, vertices, depth):
|
1219
|
+
if len(vertices) == 0:
|
1220
|
+
return None
|
1221
|
+
#points.sort(key=operator.itemgetter(depth % k))
|
1222
|
+
vertices = sortList(vertices, (depth % k))
|
1223
|
+
|
1224
|
+
middle = len(vertices) // 2
|
1225
|
+
|
1226
|
+
return BT(
|
1227
|
+
value = vertices[middle],
|
1228
|
+
left = build(
|
1229
|
+
vertices=vertices[:middle],
|
1230
|
+
depth=depth+1,
|
1231
|
+
),
|
1232
|
+
right = build(
|
1233
|
+
vertices=vertices[middle+1:],
|
1234
|
+
depth=depth+1,
|
1235
|
+
),
|
1236
|
+
)
|
1237
|
+
|
1238
|
+
return build(vertices=list(vertices), depth=0)
|
1239
|
+
|
1240
|
+
NNRecord = collections.namedtuple("NNRecord", ["vertex", "distance"])
|
1241
|
+
NNRecord.__doc__ = """
|
1242
|
+
Used to keep track of the current best guess during a nearest
|
1243
|
+
neighbor search.
|
1244
|
+
"""
|
1245
|
+
|
1246
|
+
def find_nearest_neighbor(*, tree, vertex):
|
1247
|
+
"""Find the nearest neighbor in a k-d tree for a given vertex.
|
1248
|
+
"""
|
1249
|
+
k = 3 # Forcing k to be 3 dimensional
|
1250
|
+
best = None
|
1251
|
+
def search(*, tree, depth):
|
1252
|
+
"""Recursively search through the k-d tree to find the nearest neighbor.
|
1253
|
+
"""
|
1254
|
+
nonlocal best
|
1255
|
+
|
1256
|
+
if tree is None:
|
1257
|
+
return
|
1258
|
+
distance = SED(tree.value, vertex)
|
1259
|
+
if best is None or distance < best.distance:
|
1260
|
+
best = NNRecord(vertex=tree.value, distance=distance)
|
1261
|
+
|
1262
|
+
axis = depth % k
|
1263
|
+
diff = itemAtIndex(vertex,axis) - itemAtIndex(tree.value,axis)
|
1264
|
+
if diff <= 0:
|
1265
|
+
close, away = tree.left, tree.right
|
1266
|
+
else:
|
1267
|
+
close, away = tree.right, tree.left
|
1268
|
+
|
1269
|
+
search(tree=close, depth=depth+1)
|
1270
|
+
if diff**2 < best.distance:
|
1271
|
+
search(tree=away, depth=depth+1)
|
1272
|
+
|
1273
|
+
search(tree=tree, depth=0)
|
1274
|
+
return best.vertex
|
1275
|
+
|
1276
|
+
if useKDTree:
|
1277
|
+
tree = kdtree(topology)
|
1278
|
+
return find_nearest_neighbor(tree=tree, vertex=vertex)
|
1279
|
+
else:
|
1280
|
+
vertices = []
|
1281
|
+
_ = topology.Vertices(None, vertices)
|
1282
|
+
distances = []
|
1283
|
+
indices = []
|
1284
|
+
for i in range(len(vertices)):
|
1285
|
+
distances.append(SED(vertex, vertices[i]))
|
1286
|
+
indices.append(i)
|
1287
|
+
sorted_indices = [x for _, x in sorted(zip(distances, indices))]
|
1288
|
+
return vertices[sorted_indices[0]]
|
1289
|
+
|
1290
|
+
@staticmethod
|
1291
|
+
def Origin() -> topologic.Vertex:
|
1292
|
+
"""
|
1293
|
+
Returns a vertex with coordinates (0, 0, 0)
|
1294
|
+
|
1295
|
+
Parameters
|
1296
|
+
-----------
|
1297
|
+
|
1298
|
+
Return
|
1299
|
+
-----------
|
1300
|
+
topologic.Vertex
|
1301
|
+
"""
|
1302
|
+
return Vertex.ByCoordinates(0, 0, 0)
|
1303
|
+
|
1304
|
+
@staticmethod
|
1305
|
+
def PerpendicularDistance(vertex: topologic.Vertex, face: topologic.Face, mantissa: int = 6):
|
1306
|
+
"""
|
1307
|
+
Returns the perpendicular distance between the input vertex and the input face. The face is considered to be infinite.
|
1308
|
+
|
1309
|
+
Parameters
|
1310
|
+
----------
|
1311
|
+
vertex : topologic.Vertex
|
1312
|
+
The input vertex.
|
1313
|
+
face : topologic.Face
|
1314
|
+
The input face.
|
1315
|
+
mantissa: int , optional
|
1316
|
+
The desired length of the mantissa. The default is 6.
|
1317
|
+
|
1318
|
+
Returns
|
1319
|
+
-------
|
1320
|
+
float
|
1321
|
+
The distance between the input vertex and the input topology.
|
1322
|
+
|
1323
|
+
"""
|
1324
|
+
from topologicpy.Face import Face
|
1325
|
+
import math
|
1326
|
+
|
1327
|
+
def distance_point_to_line(point, line_start, line_end):
|
1328
|
+
# Convert input points to NumPy arrays for vector operations
|
1329
|
+
point = np.array(point)
|
1330
|
+
line_start = np.array(line_start)
|
1331
|
+
line_end = np.array(line_end)
|
1332
|
+
|
1333
|
+
# Calculate the direction vector of the edge
|
1334
|
+
line_direction = line_end - line_start
|
1335
|
+
|
1336
|
+
# Vector from the edge's starting point to the point
|
1337
|
+
point_to_start = point - line_start
|
1338
|
+
|
1339
|
+
# Calculate the parameter 't' where the projection of the point onto the edge occurs
|
1340
|
+
if np.dot(line_direction, line_direction) == 0:
|
1341
|
+
t = 0
|
1342
|
+
else:
|
1343
|
+
t = np.dot(point_to_start, line_direction) / np.dot(line_direction, line_direction)
|
1344
|
+
|
1345
|
+
# Check if 't' is outside the range [0, 1], and if so, calculate distance to closest endpoint
|
1346
|
+
if t < 0:
|
1347
|
+
return np.linalg.norm(point - line_start)
|
1348
|
+
elif t > 1:
|
1349
|
+
return np.linalg.norm(point - line_end)
|
1350
|
+
|
1351
|
+
# Calculate the closest point on the edge to the given point
|
1352
|
+
closest_point = line_start + t * line_direction
|
1353
|
+
|
1354
|
+
# Calculate the distance between the closest point and the given point
|
1355
|
+
distance = np.linalg.norm(point - closest_point)
|
1356
|
+
|
1357
|
+
return distance
|
1358
|
+
if not isinstance(vertex, topologic.Vertex):
|
1359
|
+
print("Vertex.PerpendicularDistance - Error: The input vertex is not a valid topologic vertex. Returning None.")
|
1360
|
+
return None
|
1361
|
+
if not isinstance(face, topologic.Face):
|
1362
|
+
print("Vertex.PerpendicularDistance - Error: The input face is not a valid topologic face. Returning None.")
|
1363
|
+
return None
|
1364
|
+
dic = Face.PlaneEquation(face)
|
1365
|
+
if dic == None: # The face is degenerate. Try to treat as an edge.
|
1366
|
+
point = Vertex.Coordinates(vertex)
|
1367
|
+
face_vertices = Topology.Vertices(face)
|
1368
|
+
line_start = Vertex.Coordinates(face_vertices[0])
|
1369
|
+
line_end = Vertex.Coordinates(face_vertices[1])
|
1370
|
+
return round(distance_point_to_line(point, line_start, line_end), mantissa)
|
1371
|
+
a = dic["a"]
|
1372
|
+
b = dic["b"]
|
1373
|
+
c = dic["c"]
|
1374
|
+
d = dic["d"]
|
1375
|
+
x1, y1, z1 = Vertex.Coordinates(vertex)
|
1376
|
+
d = abs((a * x1 + b * y1 + c * z1 + d))
|
1377
|
+
e = (math.sqrt(a * a + b * b + c * c))
|
1378
|
+
if e == 0:
|
1379
|
+
return 0
|
1380
|
+
return round(d/e, mantissa)
|
1381
|
+
|
1382
|
+
@staticmethod
|
1383
|
+
def PlaneEquation(vertices, mantissa: int = 6):
|
1384
|
+
"""
|
1385
|
+
Returns the equation of the average plane passing through a list of vertices.
|
1386
|
+
|
1387
|
+
Parameters
|
1388
|
+
-----------
|
1389
|
+
vertices : list
|
1390
|
+
The input list of vertices
|
1391
|
+
mantissa : int , optional
|
1392
|
+
The desired length of the mantissa. The default is 6.
|
1393
|
+
|
1394
|
+
Return
|
1395
|
+
-----------
|
1396
|
+
dict
|
1397
|
+
The dictionary containing the values of a, b, c, d for the plane equation in the form of ax+by+cz+d=0.
|
1398
|
+
The keys in the dictionary are ["a", "b", "c". "d"]
|
1399
|
+
"""
|
1400
|
+
|
1401
|
+
vertices = [Vertex.Coordinates(v) for v in vertices]
|
1402
|
+
# Convert vertices to a NumPy array for easier calculations
|
1403
|
+
vertices = np.array(vertices)
|
1404
|
+
|
1405
|
+
# Calculate the centroid of the vertices
|
1406
|
+
centroid = np.mean(vertices, axis=0)
|
1407
|
+
|
1408
|
+
# Center the vertices by subtracting the centroid
|
1409
|
+
centered_vertices = vertices - centroid
|
1410
|
+
|
1411
|
+
# Calculate the covariance matrix
|
1412
|
+
covariance_matrix = np.dot(centered_vertices.T, centered_vertices)
|
1413
|
+
|
1414
|
+
# Find the normal vector by computing the eigenvector of the smallest eigenvalue
|
1415
|
+
_, eigen_vectors = np.linalg.eigh(covariance_matrix)
|
1416
|
+
normal_vector = eigen_vectors[:, 0]
|
1417
|
+
|
1418
|
+
# Normalize the normal vector
|
1419
|
+
normal_vector /= np.linalg.norm(normal_vector)
|
1420
|
+
|
1421
|
+
# Calculate the constant D using the centroid and the normal vector
|
1422
|
+
d = -np.dot(normal_vector, centroid)
|
1423
|
+
d = round(d, mantissa)
|
1424
|
+
|
1425
|
+
# Create the plane equation in the form Ax + By + Cz + D = 0
|
1426
|
+
a, b, c = normal_vector
|
1427
|
+
a = round(a, mantissa)
|
1428
|
+
b = round(b, mantissa)
|
1429
|
+
c = round(c, mantissa)
|
1430
|
+
|
1431
|
+
return {"a":a, "b":b, "c":c, "d":d}
|
1432
|
+
|
1433
|
+
@staticmethod
|
1434
|
+
def Point(x=0, y=0, z=0) -> topologic.Vertex:
|
1435
|
+
"""
|
1436
|
+
Creates a point (vertex) using the input parameters
|
1437
|
+
|
1438
|
+
Parameters
|
1439
|
+
-----------
|
1440
|
+
x : float , optional.
|
1441
|
+
The desired x coordinate. The default is 0.
|
1442
|
+
y : float , optional.
|
1443
|
+
The desired y coordinate. The default is 0.
|
1444
|
+
z : float , optional.
|
1445
|
+
The desired z coordinate. The default is 0.
|
1446
|
+
|
1447
|
+
Return
|
1448
|
+
-----------
|
1449
|
+
topologic.Vertex
|
1450
|
+
"""
|
1451
|
+
|
1452
|
+
return Vertex.ByCoordinates(x, y, z)
|
1453
|
+
|
1454
|
+
@staticmethod
|
1455
|
+
def Project(vertex: topologic.Vertex, face: topologic.Face, direction: bool = None, mantissa: int = 6) -> topologic.Vertex:
|
1456
|
+
"""
|
1457
|
+
Returns a vertex that is the projection of the input vertex unto the input face.
|
1458
|
+
|
1459
|
+
Parameters
|
1460
|
+
----------
|
1461
|
+
vertex : topologic.Vertex
|
1462
|
+
The input vertex to project unto the input face.
|
1463
|
+
face : topologic.Face
|
1464
|
+
The input face that receives the projection of the input vertex.
|
1465
|
+
direction : vector, optional
|
1466
|
+
The direction in which to project the input vertex unto the input face. If not specified, the direction of the projection is the normal of the input face. The default is None.
|
1467
|
+
mantissa : int , optional
|
1468
|
+
The length of the desired mantissa. The default is 6.
|
1469
|
+
tolerance : float , optional
|
1470
|
+
The desired tolerance. The default is 0.0001.
|
1471
|
+
|
1472
|
+
Returns
|
1473
|
+
-------
|
1474
|
+
topologic.Vertex
|
1475
|
+
The projected vertex.
|
1476
|
+
|
1477
|
+
"""
|
1478
|
+
from topologicpy.Face import Face
|
1479
|
+
|
1480
|
+
def project_point_onto_plane(point, plane_coeffs, direction_vector):
|
1481
|
+
"""
|
1482
|
+
Project a 3D point onto a plane defined by its coefficients and using a direction vector.
|
1483
|
+
|
1484
|
+
Parameters:
|
1485
|
+
point (tuple or list): The 3D point coordinates (x, y, z).
|
1486
|
+
plane_coeffs (tuple or list): The coefficients of the plane equation (a, b, c, d).
|
1487
|
+
direction_vector (tuple or list): The direction vector (vx, vy, vz).
|
1488
|
+
|
1489
|
+
Returns:
|
1490
|
+
tuple: The projected point coordinates (x_proj, y_proj, z_proj).
|
1491
|
+
"""
|
1492
|
+
# Unpack point coordinates
|
1493
|
+
x, y, z = point
|
1494
|
+
|
1495
|
+
# Unpack plane coefficients
|
1496
|
+
a, b, c, d = plane_coeffs
|
1497
|
+
|
1498
|
+
# Unpack direction vector
|
1499
|
+
vx, vy, vz = direction_vector
|
1500
|
+
|
1501
|
+
# Calculate the distance from the point to the plane
|
1502
|
+
distance = (a * x + b * y + c * z + d) / (a * vx + b * vy + c * vz)
|
1503
|
+
|
1504
|
+
# Calculate the projected point coordinates
|
1505
|
+
x_proj = x - distance * vx
|
1506
|
+
y_proj = y - distance * vy
|
1507
|
+
z_proj = z - distance * vz
|
1508
|
+
|
1509
|
+
return [x_proj, y_proj, z_proj]
|
1510
|
+
|
1511
|
+
if not isinstance(vertex, topologic.Vertex):
|
1512
|
+
return None
|
1513
|
+
if not isinstance(face, topologic.Face):
|
1514
|
+
return None
|
1515
|
+
eq = Face.PlaneEquation(face, mantissa= mantissa)
|
1516
|
+
if direction == None or direction == []:
|
1517
|
+
direction = Face.Normal(face)
|
1518
|
+
pt = project_point_onto_plane(Vertex.Coordinates(vertex), [eq["a"], eq["b"], eq["c"], eq["d"]], direction)
|
1519
|
+
return Vertex.ByCoordinates(pt[0], pt[1], pt[2])
|
1520
|
+
|
1521
|
+
@staticmethod
|
1522
|
+
def X(vertex: topologic.Vertex, mantissa: int = 6) -> float:
|
1523
|
+
"""
|
1524
|
+
Returns the X coordinate of the input vertex.
|
1525
|
+
|
1526
|
+
Parameters
|
1527
|
+
----------
|
1528
|
+
vertex : topologic.Vertex
|
1529
|
+
The input vertex.
|
1530
|
+
mantissa : int , optional
|
1531
|
+
The desired length of the mantissa. The default is 6.
|
1532
|
+
|
1533
|
+
Returns
|
1534
|
+
-------
|
1535
|
+
float
|
1536
|
+
The X coordinate of the input vertex.
|
1537
|
+
|
1538
|
+
"""
|
1539
|
+
if not isinstance(vertex, topologic.Vertex):
|
1540
|
+
return None
|
1541
|
+
return round(vertex.X(), mantissa)
|
1542
|
+
|
1543
|
+
@staticmethod
|
1544
|
+
def Y(vertex: topologic.Vertex, mantissa: int = 6) -> float:
|
1545
|
+
"""
|
1546
|
+
Returns the Y coordinate of the input vertex.
|
1547
|
+
|
1548
|
+
Parameters
|
1549
|
+
----------
|
1550
|
+
vertex : topologic.Vertex
|
1551
|
+
The input vertex.
|
1552
|
+
mantissa : int , optional
|
1553
|
+
The desired length of the mantissa. The default is 6.
|
1554
|
+
|
1555
|
+
Returns
|
1556
|
+
-------
|
1557
|
+
float
|
1558
|
+
The Y coordinate of the input vertex.
|
1559
|
+
|
1560
|
+
"""
|
1561
|
+
if not isinstance(vertex, topologic.Vertex):
|
1562
|
+
return None
|
1563
|
+
return round(vertex.Y(), mantissa)
|
1564
|
+
|
1565
|
+
@staticmethod
|
1566
|
+
def Z(vertex: topologic.Vertex, mantissa: int = 6) -> float:
|
1567
|
+
"""
|
1568
|
+
Returns the Z coordinate of the input vertex.
|
1569
|
+
|
1570
|
+
Parameters
|
1571
|
+
----------
|
1572
|
+
vertex : topologic.Vertex
|
1573
|
+
The input vertex.
|
1574
|
+
mantissa : int , optional
|
1575
|
+
The desired length of the mantissa. The default is 6.
|
1576
|
+
|
1577
|
+
Returns
|
1578
|
+
-------
|
1579
|
+
float
|
1580
|
+
The Z coordinate of the input vertex.
|
1581
|
+
|
1582
|
+
"""
|
1583
|
+
if not isinstance(vertex, topologic.Vertex):
|
1584
|
+
return None
|
1585
|
+
return round(vertex.Z(), mantissa)
|
1586
1586
|
|