topologicpy 0.5.9__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.
Files changed (94) hide show
  1. topologicpy/Aperture.py +72 -72
  2. topologicpy/Cell.py +2169 -2169
  3. topologicpy/CellComplex.py +1137 -1137
  4. topologicpy/Cluster.py +1288 -1280
  5. topologicpy/Color.py +423 -423
  6. topologicpy/Context.py +79 -79
  7. topologicpy/DGL.py +3213 -3240
  8. topologicpy/Dictionary.py +698 -698
  9. topologicpy/Edge.py +1187 -1187
  10. topologicpy/EnergyModel.py +1180 -1152
  11. topologicpy/Face.py +2141 -2141
  12. topologicpy/Graph.py +7768 -7768
  13. topologicpy/Grid.py +353 -353
  14. topologicpy/Helper.py +507 -507
  15. topologicpy/Honeybee.py +461 -461
  16. topologicpy/Matrix.py +271 -271
  17. topologicpy/Neo4j.py +521 -521
  18. topologicpy/Plotly.py +2 -2
  19. topologicpy/Polyskel.py +541 -541
  20. topologicpy/Shell.py +1768 -1768
  21. topologicpy/Speckle.py +508 -508
  22. topologicpy/Topology.py +7060 -7002
  23. topologicpy/Vector.py +905 -905
  24. topologicpy/Vertex.py +1585 -1585
  25. topologicpy/Wire.py +3050 -3050
  26. topologicpy/__init__.py +22 -38
  27. topologicpy/version.py +1 -0
  28. {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/LICENSE +661 -704
  29. topologicpy-6.0.0.dist-info/METADATA +751 -0
  30. topologicpy-6.0.0.dist-info/RECORD +32 -0
  31. topologicpy/bin/linux/topologic/__init__.py +0 -2
  32. topologicpy/bin/linux/topologic/libTKBO-6bdf205d.so.7.7.0 +0 -0
  33. topologicpy/bin/linux/topologic/libTKBRep-2960a069.so.7.7.0 +0 -0
  34. topologicpy/bin/linux/topologic/libTKBool-c44b74bd.so.7.7.0 +0 -0
  35. topologicpy/bin/linux/topologic/libTKFillet-9a670ba0.so.7.7.0 +0 -0
  36. topologicpy/bin/linux/topologic/libTKG2d-8f31849e.so.7.7.0 +0 -0
  37. topologicpy/bin/linux/topologic/libTKG3d-4c6bce57.so.7.7.0 +0 -0
  38. topologicpy/bin/linux/topologic/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
  39. topologicpy/bin/linux/topologic/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
  40. topologicpy/bin/linux/topologic/libTKMath-72572fa8.so.7.7.0 +0 -0
  41. topologicpy/bin/linux/topologic/libTKMesh-2a060427.so.7.7.0 +0 -0
  42. topologicpy/bin/linux/topologic/libTKOffset-6cab68ff.so.7.7.0 +0 -0
  43. topologicpy/bin/linux/topologic/libTKPrim-eb1262b3.so.7.7.0 +0 -0
  44. topologicpy/bin/linux/topologic/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
  45. topologicpy/bin/linux/topologic/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
  46. topologicpy/bin/linux/topologic/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
  47. topologicpy/bin/linux/topologic/libgcc_s-32c1665e.so.1 +0 -0
  48. topologicpy/bin/linux/topologic/libstdc++-672d7b41.so.6.0.30 +0 -0
  49. topologicpy/bin/linux/topologic/topologic.cpython-310-x86_64-linux-gnu.so +0 -0
  50. topologicpy/bin/linux/topologic/topologic.cpython-311-x86_64-linux-gnu.so +0 -0
  51. topologicpy/bin/linux/topologic/topologic.cpython-38-x86_64-linux-gnu.so +0 -0
  52. topologicpy/bin/linux/topologic/topologic.cpython-39-x86_64-linux-gnu.so +0 -0
  53. topologicpy/bin/linux/topologic.libs/libTKBO-6bdf205d.so.7.7.0 +0 -0
  54. topologicpy/bin/linux/topologic.libs/libTKBRep-2960a069.so.7.7.0 +0 -0
  55. topologicpy/bin/linux/topologic.libs/libTKBool-c44b74bd.so.7.7.0 +0 -0
  56. topologicpy/bin/linux/topologic.libs/libTKFillet-9a670ba0.so.7.7.0 +0 -0
  57. topologicpy/bin/linux/topologic.libs/libTKG2d-8f31849e.so.7.7.0 +0 -0
  58. topologicpy/bin/linux/topologic.libs/libTKG3d-4c6bce57.so.7.7.0 +0 -0
  59. topologicpy/bin/linux/topologic.libs/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
  60. topologicpy/bin/linux/topologic.libs/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
  61. topologicpy/bin/linux/topologic.libs/libTKMath-72572fa8.so.7.7.0 +0 -0
  62. topologicpy/bin/linux/topologic.libs/libTKMesh-2a060427.so.7.7.0 +0 -0
  63. topologicpy/bin/linux/topologic.libs/libTKOffset-6cab68ff.so.7.7.0 +0 -0
  64. topologicpy/bin/linux/topologic.libs/libTKPrim-eb1262b3.so.7.7.0 +0 -0
  65. topologicpy/bin/linux/topologic.libs/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
  66. topologicpy/bin/linux/topologic.libs/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
  67. topologicpy/bin/linux/topologic.libs/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
  68. topologicpy/bin/linux/topologic.libs/libgcc_s-32c1665e.so.1 +0 -0
  69. topologicpy/bin/linux/topologic.libs/libstdc++-672d7b41.so.6.0.30 +0 -0
  70. topologicpy/bin/macos/topologic/__init__.py +0 -2
  71. topologicpy/bin/windows/topologic/TKBO-f6b191de.dll +0 -0
  72. topologicpy/bin/windows/topologic/TKBRep-e56a600e.dll +0 -0
  73. topologicpy/bin/windows/topologic/TKBool-7b8d47ae.dll +0 -0
  74. topologicpy/bin/windows/topologic/TKFillet-0ddbf0a8.dll +0 -0
  75. topologicpy/bin/windows/topologic/TKG2d-2e2dee3d.dll +0 -0
  76. topologicpy/bin/windows/topologic/TKG3d-6674513d.dll +0 -0
  77. topologicpy/bin/windows/topologic/TKGeomAlgo-d240e370.dll +0 -0
  78. topologicpy/bin/windows/topologic/TKGeomBase-df87aba5.dll +0 -0
  79. topologicpy/bin/windows/topologic/TKMath-45bd625a.dll +0 -0
  80. topologicpy/bin/windows/topologic/TKMesh-d6e826b1.dll +0 -0
  81. topologicpy/bin/windows/topologic/TKOffset-79b9cc94.dll +0 -0
  82. topologicpy/bin/windows/topologic/TKPrim-aa430a86.dll +0 -0
  83. topologicpy/bin/windows/topologic/TKShHealing-bb48be89.dll +0 -0
  84. topologicpy/bin/windows/topologic/TKTopAlgo-7d0d1e22.dll +0 -0
  85. topologicpy/bin/windows/topologic/TKernel-08c8cfbb.dll +0 -0
  86. topologicpy/bin/windows/topologic/__init__.py +0 -2
  87. topologicpy/bin/windows/topologic/topologic.cp310-win_amd64.pyd +0 -0
  88. topologicpy/bin/windows/topologic/topologic.cp311-win_amd64.pyd +0 -0
  89. topologicpy/bin/windows/topologic/topologic.cp38-win_amd64.pyd +0 -0
  90. topologicpy/bin/windows/topologic/topologic.cp39-win_amd64.pyd +0 -0
  91. topologicpy-0.5.9.dist-info/METADATA +0 -86
  92. topologicpy-0.5.9.dist-info/RECORD +0 -91
  93. {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/WHEEL +0 -0
  94. {topologicpy-0.5.9.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