topologicpy 0.5.8__py3-none-any.whl → 6.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -393
  6. topologicpy/Context.py +79 -79
  7. topologicpy/DGL.py +3213 -3136
  8. topologicpy/Dictionary.py +698 -695
  9. topologicpy/Edge.py +1187 -1187
  10. topologicpy/EnergyModel.py +1180 -1171
  11. topologicpy/Face.py +2141 -2141
  12. topologicpy/Graph.py +7768 -7700
  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 -6988
  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.8.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.8.dist-info/METADATA +0 -96
  92. topologicpy-0.5.8.dist-info/RECORD +0 -91
  93. {topologicpy-0.5.8.dist-info → topologicpy-6.0.0.dist-info}/WHEEL +0 -0
  94. {topologicpy-0.5.8.dist-info → topologicpy-6.0.0.dist-info}/top_level.txt +0 -0
topologicpy/Vector.py CHANGED
@@ -1,905 +1,905 @@
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 math
18
- import os
19
- import warnings
20
-
21
- try:
22
- import numpy as np
23
- import numpy.linalg as la
24
- from numpy import pi, arctan2, rad2deg
25
- except:
26
- print("Vector - Installing required numpy library.")
27
- try:
28
- os.system("pip install numpy")
29
- except:
30
- os.system("pip install numpy --user")
31
- try:
32
- import numpy as np
33
- import numpy.linalg as la
34
- from numpy import pi, arctan2, rad2deg
35
- print("Vector - numpy library installed successfully.")
36
- except:
37
- warnings.warn("Vector - Error: Could not import numpy.")
38
-
39
- class Vector(list):
40
- @staticmethod
41
- def Angle(vectorA, vectorB, mantissa: int = 6):
42
- """
43
- Returns the angle in degrees between the two input vectors
44
-
45
- Parameters
46
- ----------
47
- vectorA : list
48
- The first vector.
49
- vectorB : list
50
- The second vector.
51
- mantissa : int, optional
52
- The length of the desired mantissa. The default is 4.
53
-
54
- Returns
55
- -------
56
- float
57
- The angle in degrees between the two input vectors.
58
-
59
- """
60
- n_v1=la.norm(vectorA)
61
- n_v2=la.norm(vectorB)
62
- if n_v1 == 0 or n_v2 == 0:
63
- # Handle the case where one or both vectors have a magnitude of zero.
64
- return 0.0
65
-
66
- if (abs(np.log10(n_v1/n_v2)) > 10):
67
- vectorA = vectorA/n_v1
68
- vectorB = vectorB/n_v2
69
- cosang = np.dot(vectorA, vectorB)
70
- sinang = la.norm(np.cross(vectorA, vectorB))
71
- return round(math.degrees(np.arctan2(sinang, cosang)), mantissa)
72
-
73
- @staticmethod
74
- def Average(vectors: list):
75
- """
76
- Returns the average vector of the input vectors.
77
-
78
- Parameters
79
- ----------
80
- vectors : list
81
- The input list of vectors.
82
-
83
- Returns
84
- -------
85
- list
86
- The average vector of the input list of vectors.
87
- """
88
- if not isinstance(vectors, list):
89
- print("Vector.Average - Error: The input vectors parameter is not a valid list. Returning None.")
90
- return None
91
- vectors = [vec for vec in vectors if isinstance(vec, list)]
92
- if len(vectors) < 1:
93
- print("Vector.Average - Error: The input vectors parameter does not contain any valid vectors. Returning None.")
94
- return None
95
-
96
- dimensions = len(vectors[0])
97
- num_vectors = len(vectors)
98
-
99
- # Initialize a list to store the sum of each dimension
100
- sum_dimensions = [0] * dimensions
101
-
102
- # Calculate the sum of each dimension across all vectors
103
- for vector in vectors:
104
- for i in range(dimensions):
105
- sum_dimensions[i] += vector[i]
106
-
107
- # Calculate the average for each dimension
108
- average_dimensions = [sum_dim / num_vectors for sum_dim in sum_dimensions]
109
-
110
- return average_dimensions
111
-
112
- @staticmethod
113
- def AzimuthAltitude(vector, mantissa: int = 6):
114
- """
115
- Returns a dictionary of azimuth and altitude angles in degrees for the input vector. North is assumed to be the positive Y axis [0, 1, 0]. Up is assumed to be the positive Z axis [0, 0, 1].
116
- Azimuth is calculated in a counter-clockwise fashion from North where 0 is North, 90 is East, 180 is South, and 270 is West. Altitude is calculated in a counter-clockwise fashing where -90 is straight down (negative Z axis), 0 is in the XY plane, and 90 is straight up (positive Z axis).
117
- If the altitude is -90 or 90, the azimuth is assumed to be 0.
118
-
119
- Parameters
120
- ----------
121
- vectorA : list
122
- The input vector.
123
- mantissa : int, optional
124
- The length of the desired mantissa. The default is 4.
125
-
126
- Returns
127
- -------
128
- dict
129
- The dictionary containing the azimuth and altitude angles in degrees. The keys in the dictionary are 'azimuth' and 'altitude'.
130
-
131
- """
132
- x, y, z = vector
133
- if x == 0 and y == 0:
134
- if z > 0:
135
- return {"azimuth":0, "altitude":90}
136
- elif z < 0:
137
- return {"azimuth":0, "altitude":-90}
138
- else:
139
- # undefined
140
- return None
141
- else:
142
- azimuth = math.degrees(math.atan2(y, x))
143
- if azimuth > 90:
144
- azimuth -= 360
145
- azimuth = round(90-azimuth, mantissa)
146
- xy_distance = math.sqrt(x**2 + y**2)
147
- altitude = math.degrees(math.atan2(z, xy_distance))
148
- altitude = round(altitude, mantissa)
149
- return {"azimuth":azimuth, "altitude":altitude}
150
-
151
- @staticmethod
152
- def Bisect(vectorA, vectorB):
153
- """
154
- Compute the bisecting vector of two input vectors.
155
-
156
- Parameters
157
- ----------
158
- vectorA : list
159
- The first input vector.
160
- vectorB : list
161
- The second input vector.
162
-
163
- Returns
164
- -------
165
- dict
166
- The bisecting vector.
167
-
168
- """
169
- import numpy as np
170
-
171
-
172
- # Ensure vectors are numpy arrays
173
- vector1 = np.array(vectorA)
174
- vector2 = np.array(vectorB)
175
-
176
- # Normalize input vectors
177
- vector1_norm = vector1 / np.linalg.norm(vector1)
178
- vector2_norm = vector2 / np.linalg.norm(vector2)
179
-
180
- # Check if the angle between vectors is either 0 or 180 degrees
181
- dot_product = np.dot(vector1_norm, vector2_norm)
182
- if np.isclose(dot_product, 1.0) or np.isclose(dot_product, -1.0):
183
- print("Vector.Bisect - Warning: The two vectors are collinear and thus the bisecting vector is not well-defined.")
184
- # Angle is either 0 or 180 degrees, return any perpendicular vector
185
- bisecting_vector = np.array([vector1[1] - vector1[2], vector1[2] - vector1[0], vector1[0] - vector1[1]])
186
- bisecting_vector /= np.linalg.norm(bisecting_vector)
187
- else:
188
- # Compute bisecting vector
189
- bisecting_vector = (vector1_norm + vector2_norm) / np.linalg.norm(vector1_norm + vector2_norm)
190
-
191
- return bisecting_vector
192
-
193
- @staticmethod
194
- def ByAzimuthAltitude(azimuth, altitude, north=0, reverse=False, tolerance=0.0001):
195
- """
196
- Returns the vector specified by the input azimuth and altitude angles.
197
-
198
- Parameters
199
- ----------
200
- azimuth : float
201
- The input azimuth angle in degrees. The angle is computed in an anti-clockwise fashion. 0 is considered North, 90 East, 180 is South, 270 is West
202
- altitude : float
203
- The input altitude angle in degrees from the XY plane. Positive is above the XY plane. Negative is below the XY plane
204
- north : float , optional
205
- The angle of the north direction in degrees measured from positive Y-axis. The angle is added in anti-clockwise fashion. 0 is considered along the positive Y-axis,
206
- 90 is along the positive X-axis, 180 is along the negative Y-axis, and 270 along the negative Y-axis.
207
- reverse : bool , optional
208
- If set to True the direction of the vector is computed from the end point towards the origin. Otherwise, it is computed from the origin towards the end point.
209
- tolerance : float , optional
210
- The desired tolerance. The default is 0.0001.
211
-
212
- Returns
213
- -------
214
- list
215
- The resulting vector.
216
-
217
- """
218
- from topologicpy.Vertex import Vertex
219
- from topologicpy.Edge import Edge
220
- from topologicpy.Topology import Topology
221
- e = Edge.ByVertices([Vertex.Origin(), Vertex.ByCoordinates(0, 1, 0)], tolerance=tolerance)
222
- e = Topology.Rotate(e, origin=Vertex.Origin(), axis=[1, 0, 0], angle=altitude)
223
- e = Topology.Rotate(e, origin=Vertex.Origin(), axis=[0, 0, 1], angle=-azimuth-north)
224
- if reverse:
225
- return Vector.Reverse(Edge.Direction(e))
226
- return Edge.Direction(e)
227
-
228
- @staticmethod
229
- def ByCoordinates(x, y, z):
230
- """
231
- Creates a vector by the specified x, y, z inputs.
232
-
233
- Parameters
234
- ----------
235
- x : float
236
- The X coordinate.
237
- y : float
238
- The Y coordinate.
239
- z : float
240
- The Z coodinate.
241
-
242
- Returns
243
- -------
244
- list
245
- The created vector.
246
-
247
- """
248
- return [x, y, z]
249
-
250
- @staticmethod
251
- def ByVertices(vertices, normalize=True):
252
- """
253
- Creates a vector by the specified input list of vertices.
254
-
255
- Parameters
256
- ----------
257
- vertices : list
258
- The the input list of topologic vertices. The first element in the list is considered the start vertex. The last element in the list is considered the end vertex.
259
- normalize : bool , optional
260
- If set to True, the resulting vector is normalized (i.e. its length is set to 1)
261
-
262
- Returns
263
- -------
264
- list
265
- The created vector.
266
-
267
- """
268
-
269
- from topologicpy.Vertex import Vertex
270
- import topologic
271
- if not isinstance(vertices, list):
272
- return None
273
- if not isinstance(normalize, bool):
274
- return None
275
- vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
276
- if len(vertices) < 2:
277
- return None
278
- v1 = vertices[0]
279
- v2 = vertices[-1]
280
- vector = [Vertex.X(v2)-Vertex.X(v1), Vertex.Y(v2)-Vertex.Y(v1), Vertex.Z(v2)-Vertex.Z(v1)]
281
- if normalize:
282
- vector = Vector.Normalize(vector)
283
- return vector
284
-
285
- @staticmethod
286
- def CompassAngle(vectorA, vectorB, mantissa=6, tolerance=0.0001):
287
- """
288
- Returns the horizontal compass angle in degrees between the two input vectors. The angle is measured in clockwise fashion.
289
- 0 is along the positive Y-axis, 90 is along the positive X axis.
290
- Only the first two elements in the input vectors are considered.
291
-
292
- Parameters
293
- ----------
294
- vectorA : list
295
- The first vector.
296
- vectorB : list
297
- The second vector.
298
- mantissa : int, optional
299
- The length of the desired mantissa. The default is 4.
300
- tolerance : float , optional
301
- The desired tolerance. The default is 0.0001.
302
-
303
- Returns
304
- -------
305
- float
306
- The horizontal compass angle in degrees between the two input vectors.
307
-
308
- """
309
- if abs(vectorA[0]) < tolerance and abs(vectorA[1]) < tolerance:
310
- return None
311
- if abs(vectorB[0]) < tolerance and abs(vectorB[1]) < tolerance:
312
- return None
313
- p1 = (vectorA[0], vectorA[1])
314
- p2 = (vectorB[0], vectorB[1])
315
- ang1 = arctan2(*p1[::-1])
316
- ang2 = arctan2(*p2[::-1])
317
- return round(rad2deg((ang1 - ang2) % (2 * pi)), mantissa)
318
-
319
- @staticmethod
320
- def Coordinates(vector, outputType="xyz", mantissa: int = 6):
321
- """
322
- Returns the coordinates of the input vector.
323
-
324
- Parameters
325
- ----------
326
- vector : list
327
- The input vector.
328
- outputType : string, optional
329
- 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.
330
- mantissa : int , optional
331
- The desired length of the mantissa. The default is 6.
332
-
333
- Returns
334
- -------
335
- list
336
- The coordinates of the input vertex.
337
-
338
- """
339
- if not isinstance(vector, list):
340
- return None
341
- x = round(vector[0], mantissa)
342
- y = round(vector[1], mantissa)
343
- z = round(vector[2], mantissa)
344
- matrix = [[1, 0, 0, x],
345
- [0, 1, 0, y],
346
- [0, 0, 1, z],
347
- [0, 0, 0, 1]]
348
- output = []
349
- outputType = outputType.lower()
350
- if outputType == "matrix":
351
- return matrix
352
- else:
353
- outputType = list(outputType)
354
- for axis in outputType:
355
- if axis == "x":
356
- output.append(x)
357
- elif axis == "y":
358
- output.append(y)
359
- elif axis == "z":
360
- output.append(z)
361
- return output
362
-
363
- @staticmethod
364
- def Cross(vectorA, vectorB, mantissa=6, tolerance=0.0001):
365
- """
366
- Returns the cross product of the two input vectors. The resulting vector is perpendicular to the plane defined by the two input vectors.
367
-
368
- Parameters
369
- ----------
370
- vectorA : list
371
- The first vector.
372
- vectorB : list
373
- The second vector.
374
- mantissa : int, optional
375
- The length of the desired mantissa. The default is 4.
376
- tolerance : float, optional
377
- the desired tolerance. The default is 0.0001.
378
-
379
- Returns
380
- -------
381
- list
382
- The vector representing the cross product of the two input vectors.
383
-
384
- """
385
- if not isinstance(vectorA, list) or not isinstance(vectorB, list):
386
- return None
387
- if Vector.Magnitude(vector=vectorA, mantissa=mantissa) < tolerance or Vector.Magnitude(vector=vectorB, mantissa=mantissa) < tolerance:
388
- return None
389
- vecA = np.array(vectorA)
390
- vecB = np.array(vectorB)
391
- vecC = list(np.cross(vecA, vecB))
392
- if Vector.Magnitude(vecC) < tolerance:
393
- return [0, 0, 0]
394
- return [round(vecC[0], mantissa), round(vecC[1], mantissa), round(vecC[2], mantissa)]
395
-
396
- @staticmethod
397
- def Down():
398
- """
399
- Returns the vector representing the *down* direction. In Topologic, the negative ZAxis direction is considered *down* ([0, 0, -1]).
400
-
401
- Returns
402
- -------
403
- list
404
- The vector representing the *down* direction.
405
- """
406
- return [0, 0, -1]
407
-
408
- @staticmethod
409
- def East():
410
- """
411
- Returns the vector representing the *east* direction. In Topologic, the positive XAxis direction is considered *east* ([1, 0, 0]).
412
-
413
- Returns
414
- -------
415
- list
416
- The vector representing the *east* direction.
417
- """
418
- return [1, 0, 0]
419
-
420
- @staticmethod
421
- def IsAntiParallel(vectorA, vectorB):
422
- """
423
- Returns True if the input vectors are anti-parallel. Returns False otherwise.
424
-
425
- Parameters
426
- ----------
427
- vectorA : list
428
- The first input vector.
429
- vectorB : list
430
- The second input vector.
431
-
432
- Returns
433
- -------
434
- bool
435
- True if the input vectors are anti-parallel. False otherwise.
436
-
437
- """
438
- import numpy as np
439
-
440
-
441
- # Ensure vectors are numpy arrays
442
- vector1 = np.array(vectorA)
443
- vector2 = np.array(vectorB)
444
-
445
- # Normalize input vectors
446
- vector1_norm = vector1 / np.linalg.norm(vector1)
447
- vector2_norm = vector2 / np.linalg.norm(vector2)
448
-
449
- # Check if the angle between vectors is either 0 or 180 degrees
450
- dot_product = np.dot(vector1_norm, vector2_norm)
451
- if np.isclose(dot_product, -1.0):
452
- return True
453
- else:
454
- # Compute bisecting vector
455
- return False
456
-
457
- @staticmethod
458
- def IsParallel(vectorA, vectorB):
459
- """
460
- Returns True if the input vectors are parallel. Returns False otherwise.
461
-
462
- Parameters
463
- ----------
464
- vectorA : list
465
- The first input vector.
466
- vectorB : list
467
- The second input vector.
468
-
469
- Returns
470
- -------
471
- bool
472
- True if the input vectors are parallel. False otherwise.
473
-
474
- """
475
- import numpy as np
476
-
477
-
478
- # Ensure vectors are numpy arrays
479
- vector1 = np.array(vectorA)
480
- vector2 = np.array(vectorB)
481
-
482
- # Normalize input vectors
483
- vector1_norm = vector1 / np.linalg.norm(vector1)
484
- vector2_norm = vector2 / np.linalg.norm(vector2)
485
-
486
- # Check if the angle between vectors is either 0 or 180 degrees
487
- dot_product = np.dot(vector1_norm, vector2_norm)
488
- if np.isclose(dot_product, 1.0):
489
- return True
490
- else:
491
- # Compute bisecting vector
492
- return False
493
-
494
- @staticmethod
495
- def IsCollinear(vectorA, vectorB):
496
- """
497
- Returns True if the input vectors are collinear (parallel or anti-parallel). Returns False otherwise.
498
-
499
- Parameters
500
- ----------
501
- vectorA : list
502
- The first input vector.
503
- vectorB : list
504
- The second input vector.
505
-
506
- Returns
507
- -------
508
- bool
509
- True if the input vectors are collinear (parallel or anti-parallel). False otherwise.
510
-
511
- """
512
- import numpy as np
513
-
514
-
515
- # Ensure vectors are numpy arrays
516
- vector1 = np.array(vectorA)
517
- vector2 = np.array(vectorB)
518
-
519
- # Normalize input vectors
520
- vector1_norm = vector1 / np.linalg.norm(vector1)
521
- vector2_norm = vector2 / np.linalg.norm(vector2)
522
-
523
- # Check if the angle between vectors is either 0 or 180 degrees
524
- dot_product = np.dot(vector1_norm, vector2_norm)
525
- if np.isclose(dot_product, 1.0) or np.isclose(dot_product, -1.0):
526
- return True
527
- else:
528
- # Compute bisecting vector
529
- return False
530
-
531
- @staticmethod
532
- def IsParallel(vectorA, vectorB):
533
- """
534
- Returns True if the input vectors are parallel. Returns False otherwise.
535
-
536
- Parameters
537
- ----------
538
- vectorA : list
539
- The first input vector.
540
- vectorB : list
541
- The second input vector.
542
-
543
- Returns
544
- -------
545
- bool
546
- True if the input vectors are parallel. False otherwise.
547
-
548
- """
549
- import numpy as np
550
-
551
-
552
- # Ensure vectors are numpy arrays
553
- vector1 = np.array(vectorA)
554
- vector2 = np.array(vectorB)
555
-
556
- # Normalize input vectors
557
- vector1_norm = vector1 / np.linalg.norm(vector1)
558
- vector2_norm = vector2 / np.linalg.norm(vector2)
559
-
560
- # Check if the angle between vectors is either 0 or 180 degrees
561
- dot_product = np.dot(vector1_norm, vector2_norm)
562
- if np.isclose(dot_product, 1.0):
563
- return True
564
- else:
565
- # Compute bisecting vector
566
- return False
567
-
568
- @staticmethod
569
- def Magnitude(vector, mantissa: int = 6):
570
- """
571
- Returns the magnitude of the input vector.
572
-
573
- Parameters
574
- ----------
575
- vector : list
576
- The input vector.
577
- mantissa : int
578
- The length of the desired mantissa. The default is 4.
579
-
580
- Returns
581
- -------
582
- float
583
- The magnitude of the input vector.
584
- """
585
-
586
- return round(np.linalg.norm(np.array(vector)), mantissa)
587
-
588
- @staticmethod
589
- def Multiply(vector, magnitude, tolerance=0.0001):
590
- """
591
- Multiplies the input vector by the input magnitude.
592
-
593
- Parameters
594
- ----------
595
- vector : list
596
- The input vector.
597
- magnitude : float
598
- The input magnitude.
599
- tolerance : float, optional
600
- the desired tolerance. The default is 0.0001.
601
-
602
- Returns
603
- -------
604
- list
605
- The created vector that multiplies the input vector by the input magnitude.
606
-
607
- """
608
- if abs(magnitude) < tolerance:
609
- return [0.0] * len(vector)
610
- scaled_vector = [component * (magnitude) for component in vector]
611
- return scaled_vector
612
-
613
-
614
- @staticmethod
615
- def Normalize(vector):
616
- """
617
- Returns a normalized vector of the input vector. A normalized vector has the same direction as the input vector, but its magnitude is 1.
618
-
619
- Parameters
620
- ----------
621
- vector : list
622
- The input vector.
623
-
624
- Returns
625
- -------
626
- list
627
- The normalized vector.
628
- """
629
-
630
- return list(vector / np.linalg.norm(vector))
631
-
632
- @staticmethod
633
- def North():
634
- """
635
- Returns the vector representing the *north* direction. In Topologic, the positive YAxis direction is considered *north* ([0, 1, 0]).
636
-
637
- Returns
638
- -------
639
- list
640
- The vector representing the *north* direction.
641
- """
642
- return [0, 1, 0]
643
-
644
- @staticmethod
645
- def NorthEast():
646
- """
647
- Returns the vector representing the *northeast* direction. In Topologic, the positive YAxis direction is considered *north* and the positive XAxis direction is considered *east*. Therefore *northeast* is ([1, 1, 0]).
648
-
649
- Returns
650
- -------
651
- list
652
- The vector representing the *northeast* direction.
653
- """
654
- return [1, 1, 0]
655
-
656
- @staticmethod
657
- def NorthWest():
658
- """
659
- Returns the vector representing the *northwest* direction. In Topologic, the positive YAxis direction is considered *north* and the negative XAxis direction is considered *west*. Therefore *northwest* is ([-1, 1, 0]).
660
-
661
- Returns
662
- -------
663
- list
664
- The vector representing the *northwest* direction.
665
- """
666
- return [-1, 1, 0]
667
-
668
- @staticmethod
669
- def Reverse(vector):
670
- """
671
- Returns a reverse vector of the input vector. A reverse vector multiplies all components by -1.
672
-
673
- Parameters
674
- ----------
675
- vector : list
676
- The input vector.
677
-
678
- Returns
679
- -------
680
- list
681
- The normalized vector.
682
- """
683
- if not isinstance(vector, list):
684
- return None
685
- return [x*-1 for x in vector]
686
-
687
- @staticmethod
688
- def SetMagnitude(vector: list, magnitude: float) -> list:
689
- """
690
- Sets the magnitude of the input vector to the input magnitude.
691
-
692
- Parameters
693
- ----------
694
- vector : list
695
- The input vector.
696
- magnitude : float
697
- The desired magnitude.
698
-
699
- Returns
700
- -------
701
- list
702
- The created vector.
703
- """
704
- return (Vector.Multiply(vector=Vector.Normalize(vector), magnitude=magnitude))
705
-
706
- @staticmethod
707
- def South():
708
- """
709
- Returns the vector representing the *south* direction. In Topologic, the negative YAxis direction is considered *south* ([0, -1, 0]).
710
-
711
- Returns
712
- -------
713
- list
714
- The vector representing the *south* direction.
715
- """
716
- return [0, -1, 0]
717
-
718
- @staticmethod
719
- def SouthEast():
720
- """
721
- Returns the vector representing the *southeast* direction. In Topologic, the negative YAxis direction is considered *south* and the positive XAxis direction is considered *east*. Therefore *southeast* is ([1, -1, 0]).
722
-
723
- Returns
724
- -------
725
- list
726
- The vector representing the *southeast* direction.
727
- """
728
- return [1, -1, 0]
729
-
730
- @staticmethod
731
- def SouthWest():
732
- """
733
- Returns the vector representing the *southwest* direction. In Topologic, the negative YAxis direction is considered *south* and the negative XAxis direction is considered *west*. Therefore *southwest* is ([-1, -1, 0]).
734
-
735
- Returns
736
- -------
737
- list
738
- The vector representing the *southwest* direction.
739
- """
740
- return [-1, -1, 0]
741
-
742
- @staticmethod
743
- def Sum(vectors: list):
744
- """
745
- Returns the sum vector of the input vectors.
746
-
747
- Parameters
748
- ----------
749
- vectors : list
750
- The input list of vectors.
751
-
752
- Returns
753
- -------
754
- list
755
- The sum vector of the input list of vectors.
756
- """
757
- if not isinstance(vectors, list):
758
- print("Vector.Average - Error: The input vectors parameter is not a valid list. Returning None.")
759
- return None
760
- vectors = [vec for vec in vectors if isinstance(vec, list)]
761
- if len(vectors) < 1:
762
- print("Vector.Average - Error: The input vectors parameter does not contain any valid vectors. Returning None.")
763
- return None
764
-
765
- dimensions = len(vectors[0])
766
- num_vectors = len(vectors)
767
-
768
- # Initialize a list to store the sum of each dimension
769
- sum_dimensions = [0] * dimensions
770
-
771
- # Calculate the sum of each dimension across all vectors
772
- for vector in vectors:
773
- for i in range(dimensions):
774
- sum_dimensions[i] += vector[i]
775
-
776
- return sum_dimensions
777
-
778
- @staticmethod
779
- def TransformationMatrix(vectorA, vectorB):
780
- """
781
- Returns the transformation matrix needed to align vectorA with vectorB.
782
-
783
- Parameters
784
- ----------
785
- vectorA : list
786
- The input vector to be transformed.
787
- vectorB : list
788
- The desired vector with which to align vectorA.
789
-
790
- Returns
791
- -------
792
- list
793
- Transformation matrix that follows the Blender software convention (nested list)
794
- """
795
- import numpy as np
796
- from topologicpy.Matrix import Matrix
797
-
798
- import numpy as np
799
-
800
- def transformation_matrix(vec1, vec2, translation_vector=None):
801
- """
802
- Compute a 4x4 transformation matrix that aligns vec1 to vec2.
803
-
804
- :param vec1: A 3D "source" vector
805
- :param vec2: A 3D "destination" vector
806
- :param translation_vector: Optional translation vector (default is None)
807
- :return: The 4x4 transformation matrix
808
- """
809
- vec1 = vec1 / np.linalg.norm(vec1)
810
- vec2 = vec2 / np.linalg.norm(vec2)
811
- dot_product = np.dot(vec1, vec2)
812
-
813
- if np.isclose(dot_product, 1.0):
814
- # Vectors are parallel; return the identity matrix
815
- return np.eye(4)
816
- elif np.isclose(dot_product, -1.0):
817
- # Vectors are antiparallel; reflect one of the vectors about the origin
818
- reflection_matrix = np.eye(4)
819
- reflection_matrix[2, 2] = -1
820
- return reflection_matrix
821
-
822
- cross_product = np.cross(vec1, vec2)
823
-
824
- skew_symmetric_matrix = np.array([[0, -cross_product[2], cross_product[1], 0],
825
- [cross_product[2], 0, -cross_product[0], 0],
826
- [-cross_product[1], cross_product[0], 0, 0],
827
- [0, 0, 0, 1]])
828
-
829
- rotation_matrix = np.eye(4) + skew_symmetric_matrix + \
830
- np.dot(skew_symmetric_matrix, skew_symmetric_matrix) * \
831
- (1 / (1 + dot_product))
832
-
833
- if translation_vector is not None:
834
- translation_matrix = np.eye(4)
835
- translation_matrix[:3, 3] = translation_vector
836
- transformation_matrix = np.dot(translation_matrix, rotation_matrix)
837
- else:
838
- transformation_matrix = rotation_matrix
839
-
840
- return transformation_matrix
841
-
842
-
843
- tran_mat = transformation_matrix(vectorA, vectorB)
844
-
845
- return [list(tran_mat[0]), list(tran_mat[1]), list(tran_mat[2]), list(tran_mat[3])]
846
-
847
- @staticmethod
848
- def Up():
849
- """
850
- Returns the vector representing the up direction. In Topologic, the positive ZAxis direction is considered "up" ([0, 0, 1]).
851
-
852
- Returns
853
- -------
854
- list
855
- The vector representing the "up" direction.
856
- """
857
- return [0, 0, 1]
858
-
859
- @staticmethod
860
- def West():
861
- """
862
- Returns the vector representing the *west* direction. In Topologic, the negative XAxis direction is considered *west* ([-1, 0, 0]).
863
-
864
- Returns
865
- -------
866
- list
867
- The vector representing the *west* direction.
868
- """
869
- return [-1, 0, 0]
870
-
871
- @staticmethod
872
- def XAxis():
873
- """
874
- Returns the vector representing the XAxis ([1, 0, 0])
875
-
876
- Returns
877
- -------
878
- list
879
- The vector representing the XAxis.
880
- """
881
- return [1, 0, 0]
882
-
883
- @staticmethod
884
- def YAxis():
885
- """
886
- Returns the vector representing the YAxis ([0, 1, 0])
887
-
888
- Returns
889
- -------
890
- list
891
- The vector representing the YAxis.
892
- """
893
- return [0, 1, 0]
894
-
895
- @staticmethod
896
- def ZAxis():
897
- """
898
- Returns the vector representing the ZAxis ([0, 0, 1])
899
-
900
- Returns
901
- -------
902
- list
903
- The vector representing the ZAxis.
904
- """
905
- return [0, 0, 1]
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 math
18
+ import os
19
+ import warnings
20
+
21
+ try:
22
+ import numpy as np
23
+ import numpy.linalg as la
24
+ from numpy import pi, arctan2, rad2deg
25
+ except:
26
+ print("Vector - Installing required numpy library.")
27
+ try:
28
+ os.system("pip install numpy")
29
+ except:
30
+ os.system("pip install numpy --user")
31
+ try:
32
+ import numpy as np
33
+ import numpy.linalg as la
34
+ from numpy import pi, arctan2, rad2deg
35
+ print("Vector - numpy library installed successfully.")
36
+ except:
37
+ warnings.warn("Vector - Error: Could not import numpy.")
38
+
39
+ class Vector(list):
40
+ @staticmethod
41
+ def Angle(vectorA, vectorB, mantissa: int = 6):
42
+ """
43
+ Returns the angle in degrees between the two input vectors
44
+
45
+ Parameters
46
+ ----------
47
+ vectorA : list
48
+ The first vector.
49
+ vectorB : list
50
+ The second vector.
51
+ mantissa : int, optional
52
+ The length of the desired mantissa. The default is 6.
53
+
54
+ Returns
55
+ -------
56
+ float
57
+ The angle in degrees between the two input vectors.
58
+
59
+ """
60
+ n_v1=la.norm(vectorA)
61
+ n_v2=la.norm(vectorB)
62
+ if n_v1 == 0 or n_v2 == 0:
63
+ # Handle the case where one or both vectors have a magnitude of zero.
64
+ return 0.0
65
+
66
+ if (abs(np.log10(n_v1/n_v2)) > 10):
67
+ vectorA = vectorA/n_v1
68
+ vectorB = vectorB/n_v2
69
+ cosang = np.dot(vectorA, vectorB)
70
+ sinang = la.norm(np.cross(vectorA, vectorB))
71
+ return round(math.degrees(np.arctan2(sinang, cosang)), mantissa)
72
+
73
+ @staticmethod
74
+ def Average(vectors: list):
75
+ """
76
+ Returns the average vector of the input vectors.
77
+
78
+ Parameters
79
+ ----------
80
+ vectors : list
81
+ The input list of vectors.
82
+
83
+ Returns
84
+ -------
85
+ list
86
+ The average vector of the input list of vectors.
87
+ """
88
+ if not isinstance(vectors, list):
89
+ print("Vector.Average - Error: The input vectors parameter is not a valid list. Returning None.")
90
+ return None
91
+ vectors = [vec for vec in vectors if isinstance(vec, list)]
92
+ if len(vectors) < 1:
93
+ print("Vector.Average - Error: The input vectors parameter does not contain any valid vectors. Returning None.")
94
+ return None
95
+
96
+ dimensions = len(vectors[0])
97
+ num_vectors = len(vectors)
98
+
99
+ # Initialize a list to store the sum of each dimension
100
+ sum_dimensions = [0] * dimensions
101
+
102
+ # Calculate the sum of each dimension across all vectors
103
+ for vector in vectors:
104
+ for i in range(dimensions):
105
+ sum_dimensions[i] += vector[i]
106
+
107
+ # Calculate the average for each dimension
108
+ average_dimensions = [sum_dim / num_vectors for sum_dim in sum_dimensions]
109
+
110
+ return average_dimensions
111
+
112
+ @staticmethod
113
+ def AzimuthAltitude(vector, mantissa: int = 6):
114
+ """
115
+ Returns a dictionary of azimuth and altitude angles in degrees for the input vector. North is assumed to be the positive Y axis [0, 1, 0]. Up is assumed to be the positive Z axis [0, 0, 1].
116
+ Azimuth is calculated in a counter-clockwise fashion from North where 0 is North, 90 is East, 180 is South, and 270 is West. Altitude is calculated in a counter-clockwise fashing where -90 is straight down (negative Z axis), 0 is in the XY plane, and 90 is straight up (positive Z axis).
117
+ If the altitude is -90 or 90, the azimuth is assumed to be 0.
118
+
119
+ Parameters
120
+ ----------
121
+ vectorA : list
122
+ The input vector.
123
+ mantissa : int, optional
124
+ The length of the desired mantissa. The default is 6.
125
+
126
+ Returns
127
+ -------
128
+ dict
129
+ The dictionary containing the azimuth and altitude angles in degrees. The keys in the dictionary are 'azimuth' and 'altitude'.
130
+
131
+ """
132
+ x, y, z = vector
133
+ if x == 0 and y == 0:
134
+ if z > 0:
135
+ return {"azimuth":0, "altitude":90}
136
+ elif z < 0:
137
+ return {"azimuth":0, "altitude":-90}
138
+ else:
139
+ # undefined
140
+ return None
141
+ else:
142
+ azimuth = math.degrees(math.atan2(y, x))
143
+ if azimuth > 90:
144
+ azimuth -= 360
145
+ azimuth = round(90-azimuth, mantissa)
146
+ xy_distance = math.sqrt(x**2 + y**2)
147
+ altitude = math.degrees(math.atan2(z, xy_distance))
148
+ altitude = round(altitude, mantissa)
149
+ return {"azimuth":azimuth, "altitude":altitude}
150
+
151
+ @staticmethod
152
+ def Bisect(vectorA, vectorB):
153
+ """
154
+ Compute the bisecting vector of two input vectors.
155
+
156
+ Parameters
157
+ ----------
158
+ vectorA : list
159
+ The first input vector.
160
+ vectorB : list
161
+ The second input vector.
162
+
163
+ Returns
164
+ -------
165
+ dict
166
+ The bisecting vector.
167
+
168
+ """
169
+ import numpy as np
170
+
171
+
172
+ # Ensure vectors are numpy arrays
173
+ vector1 = np.array(vectorA)
174
+ vector2 = np.array(vectorB)
175
+
176
+ # Normalize input vectors
177
+ vector1_norm = vector1 / np.linalg.norm(vector1)
178
+ vector2_norm = vector2 / np.linalg.norm(vector2)
179
+
180
+ # Check if the angle between vectors is either 0 or 180 degrees
181
+ dot_product = np.dot(vector1_norm, vector2_norm)
182
+ if np.isclose(dot_product, 1.0) or np.isclose(dot_product, -1.0):
183
+ print("Vector.Bisect - Warning: The two vectors are collinear and thus the bisecting vector is not well-defined.")
184
+ # Angle is either 0 or 180 degrees, return any perpendicular vector
185
+ bisecting_vector = np.array([vector1[1] - vector1[2], vector1[2] - vector1[0], vector1[0] - vector1[1]])
186
+ bisecting_vector /= np.linalg.norm(bisecting_vector)
187
+ else:
188
+ # Compute bisecting vector
189
+ bisecting_vector = (vector1_norm + vector2_norm) / np.linalg.norm(vector1_norm + vector2_norm)
190
+
191
+ return bisecting_vector
192
+
193
+ @staticmethod
194
+ def ByAzimuthAltitude(azimuth, altitude, north=0, reverse=False, tolerance=0.0001):
195
+ """
196
+ Returns the vector specified by the input azimuth and altitude angles.
197
+
198
+ Parameters
199
+ ----------
200
+ azimuth : float
201
+ The input azimuth angle in degrees. The angle is computed in an anti-clockwise fashion. 0 is considered North, 90 East, 180 is South, 270 is West
202
+ altitude : float
203
+ The input altitude angle in degrees from the XY plane. Positive is above the XY plane. Negative is below the XY plane
204
+ north : float , optional
205
+ The angle of the north direction in degrees measured from positive Y-axis. The angle is added in anti-clockwise fashion. 0 is considered along the positive Y-axis,
206
+ 90 is along the positive X-axis, 180 is along the negative Y-axis, and 270 along the negative Y-axis.
207
+ reverse : bool , optional
208
+ If set to True the direction of the vector is computed from the end point towards the origin. Otherwise, it is computed from the origin towards the end point.
209
+ tolerance : float , optional
210
+ The desired tolerance. The default is 0.0001.
211
+
212
+ Returns
213
+ -------
214
+ list
215
+ The resulting vector.
216
+
217
+ """
218
+ from topologicpy.Vertex import Vertex
219
+ from topologicpy.Edge import Edge
220
+ from topologicpy.Topology import Topology
221
+ e = Edge.ByVertices([Vertex.Origin(), Vertex.ByCoordinates(0, 1, 0)], tolerance=tolerance)
222
+ e = Topology.Rotate(e, origin=Vertex.Origin(), axis=[1, 0, 0], angle=altitude)
223
+ e = Topology.Rotate(e, origin=Vertex.Origin(), axis=[0, 0, 1], angle=-azimuth-north)
224
+ if reverse:
225
+ return Vector.Reverse(Edge.Direction(e))
226
+ return Edge.Direction(e)
227
+
228
+ @staticmethod
229
+ def ByCoordinates(x, y, z):
230
+ """
231
+ Creates a vector by the specified x, y, z inputs.
232
+
233
+ Parameters
234
+ ----------
235
+ x : float
236
+ The X coordinate.
237
+ y : float
238
+ The Y coordinate.
239
+ z : float
240
+ The Z coodinate.
241
+
242
+ Returns
243
+ -------
244
+ list
245
+ The created vector.
246
+
247
+ """
248
+ return [x, y, z]
249
+
250
+ @staticmethod
251
+ def ByVertices(vertices, normalize=True):
252
+ """
253
+ Creates a vector by the specified input list of vertices.
254
+
255
+ Parameters
256
+ ----------
257
+ vertices : list
258
+ The the input list of topologic vertices. The first element in the list is considered the start vertex. The last element in the list is considered the end vertex.
259
+ normalize : bool , optional
260
+ If set to True, the resulting vector is normalized (i.e. its length is set to 1)
261
+
262
+ Returns
263
+ -------
264
+ list
265
+ The created vector.
266
+
267
+ """
268
+
269
+ from topologicpy.Vertex import Vertex
270
+ import topologic_core as topologic
271
+ if not isinstance(vertices, list):
272
+ return None
273
+ if not isinstance(normalize, bool):
274
+ return None
275
+ vertices = [v for v in vertices if isinstance(v, topologic.Vertex)]
276
+ if len(vertices) < 2:
277
+ return None
278
+ v1 = vertices[0]
279
+ v2 = vertices[-1]
280
+ vector = [Vertex.X(v2)-Vertex.X(v1), Vertex.Y(v2)-Vertex.Y(v1), Vertex.Z(v2)-Vertex.Z(v1)]
281
+ if normalize:
282
+ vector = Vector.Normalize(vector)
283
+ return vector
284
+
285
+ @staticmethod
286
+ def CompassAngle(vectorA, vectorB, mantissa=6, tolerance=0.0001):
287
+ """
288
+ Returns the horizontal compass angle in degrees between the two input vectors. The angle is measured in clockwise fashion.
289
+ 0 is along the positive Y-axis, 90 is along the positive X axis.
290
+ Only the first two elements in the input vectors are considered.
291
+
292
+ Parameters
293
+ ----------
294
+ vectorA : list
295
+ The first vector.
296
+ vectorB : list
297
+ The second vector.
298
+ mantissa : int, optional
299
+ The length of the desired mantissa. The default is 6.
300
+ tolerance : float , optional
301
+ The desired tolerance. The default is 0.0001.
302
+
303
+ Returns
304
+ -------
305
+ float
306
+ The horizontal compass angle in degrees between the two input vectors.
307
+
308
+ """
309
+ if abs(vectorA[0]) < tolerance and abs(vectorA[1]) < tolerance:
310
+ return None
311
+ if abs(vectorB[0]) < tolerance and abs(vectorB[1]) < tolerance:
312
+ return None
313
+ p1 = (vectorA[0], vectorA[1])
314
+ p2 = (vectorB[0], vectorB[1])
315
+ ang1 = arctan2(*p1[::-1])
316
+ ang2 = arctan2(*p2[::-1])
317
+ return round(rad2deg((ang1 - ang2) % (2 * pi)), mantissa)
318
+
319
+ @staticmethod
320
+ def Coordinates(vector, outputType="xyz", mantissa: int = 6):
321
+ """
322
+ Returns the coordinates of the input vector.
323
+
324
+ Parameters
325
+ ----------
326
+ vector : list
327
+ The input vector.
328
+ outputType : string, optional
329
+ 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.
330
+ mantissa : int , optional
331
+ The desired length of the mantissa. The default is 6.
332
+
333
+ Returns
334
+ -------
335
+ list
336
+ The coordinates of the input vertex.
337
+
338
+ """
339
+ if not isinstance(vector, list):
340
+ return None
341
+ x = round(vector[0], mantissa)
342
+ y = round(vector[1], mantissa)
343
+ z = round(vector[2], mantissa)
344
+ matrix = [[1, 0, 0, x],
345
+ [0, 1, 0, y],
346
+ [0, 0, 1, z],
347
+ [0, 0, 0, 1]]
348
+ output = []
349
+ outputType = outputType.lower()
350
+ if outputType == "matrix":
351
+ return matrix
352
+ else:
353
+ outputType = list(outputType)
354
+ for axis in outputType:
355
+ if axis == "x":
356
+ output.append(x)
357
+ elif axis == "y":
358
+ output.append(y)
359
+ elif axis == "z":
360
+ output.append(z)
361
+ return output
362
+
363
+ @staticmethod
364
+ def Cross(vectorA, vectorB, mantissa=6, tolerance=0.0001):
365
+ """
366
+ Returns the cross product of the two input vectors. The resulting vector is perpendicular to the plane defined by the two input vectors.
367
+
368
+ Parameters
369
+ ----------
370
+ vectorA : list
371
+ The first vector.
372
+ vectorB : list
373
+ The second vector.
374
+ mantissa : int, optional
375
+ The length of the desired mantissa. The default is 6.
376
+ tolerance : float, optional
377
+ the desired tolerance. The default is 0.0001.
378
+
379
+ Returns
380
+ -------
381
+ list
382
+ The vector representing the cross product of the two input vectors.
383
+
384
+ """
385
+ if not isinstance(vectorA, list) or not isinstance(vectorB, list):
386
+ return None
387
+ if Vector.Magnitude(vector=vectorA, mantissa=mantissa) < tolerance or Vector.Magnitude(vector=vectorB, mantissa=mantissa) < tolerance:
388
+ return None
389
+ vecA = np.array(vectorA)
390
+ vecB = np.array(vectorB)
391
+ vecC = list(np.cross(vecA, vecB))
392
+ if Vector.Magnitude(vecC) < tolerance:
393
+ return [0, 0, 0]
394
+ return [round(vecC[0], mantissa), round(vecC[1], mantissa), round(vecC[2], mantissa)]
395
+
396
+ @staticmethod
397
+ def Down():
398
+ """
399
+ Returns the vector representing the *down* direction. In Topologic, the negative ZAxis direction is considered *down* ([0, 0, -1]).
400
+
401
+ Returns
402
+ -------
403
+ list
404
+ The vector representing the *down* direction.
405
+ """
406
+ return [0, 0, -1]
407
+
408
+ @staticmethod
409
+ def East():
410
+ """
411
+ Returns the vector representing the *east* direction. In Topologic, the positive XAxis direction is considered *east* ([1, 0, 0]).
412
+
413
+ Returns
414
+ -------
415
+ list
416
+ The vector representing the *east* direction.
417
+ """
418
+ return [1, 0, 0]
419
+
420
+ @staticmethod
421
+ def IsAntiParallel(vectorA, vectorB):
422
+ """
423
+ Returns True if the input vectors are anti-parallel. Returns False otherwise.
424
+
425
+ Parameters
426
+ ----------
427
+ vectorA : list
428
+ The first input vector.
429
+ vectorB : list
430
+ The second input vector.
431
+
432
+ Returns
433
+ -------
434
+ bool
435
+ True if the input vectors are anti-parallel. False otherwise.
436
+
437
+ """
438
+ import numpy as np
439
+
440
+
441
+ # Ensure vectors are numpy arrays
442
+ vector1 = np.array(vectorA)
443
+ vector2 = np.array(vectorB)
444
+
445
+ # Normalize input vectors
446
+ vector1_norm = vector1 / np.linalg.norm(vector1)
447
+ vector2_norm = vector2 / np.linalg.norm(vector2)
448
+
449
+ # Check if the angle between vectors is either 0 or 180 degrees
450
+ dot_product = np.dot(vector1_norm, vector2_norm)
451
+ if np.isclose(dot_product, -1.0):
452
+ return True
453
+ else:
454
+ # Compute bisecting vector
455
+ return False
456
+
457
+ @staticmethod
458
+ def IsParallel(vectorA, vectorB):
459
+ """
460
+ Returns True if the input vectors are parallel. Returns False otherwise.
461
+
462
+ Parameters
463
+ ----------
464
+ vectorA : list
465
+ The first input vector.
466
+ vectorB : list
467
+ The second input vector.
468
+
469
+ Returns
470
+ -------
471
+ bool
472
+ True if the input vectors are parallel. False otherwise.
473
+
474
+ """
475
+ import numpy as np
476
+
477
+
478
+ # Ensure vectors are numpy arrays
479
+ vector1 = np.array(vectorA)
480
+ vector2 = np.array(vectorB)
481
+
482
+ # Normalize input vectors
483
+ vector1_norm = vector1 / np.linalg.norm(vector1)
484
+ vector2_norm = vector2 / np.linalg.norm(vector2)
485
+
486
+ # Check if the angle between vectors is either 0 or 180 degrees
487
+ dot_product = np.dot(vector1_norm, vector2_norm)
488
+ if np.isclose(dot_product, 1.0):
489
+ return True
490
+ else:
491
+ # Compute bisecting vector
492
+ return False
493
+
494
+ @staticmethod
495
+ def IsCollinear(vectorA, vectorB):
496
+ """
497
+ Returns True if the input vectors are collinear (parallel or anti-parallel). Returns False otherwise.
498
+
499
+ Parameters
500
+ ----------
501
+ vectorA : list
502
+ The first input vector.
503
+ vectorB : list
504
+ The second input vector.
505
+
506
+ Returns
507
+ -------
508
+ bool
509
+ True if the input vectors are collinear (parallel or anti-parallel). False otherwise.
510
+
511
+ """
512
+ import numpy as np
513
+
514
+
515
+ # Ensure vectors are numpy arrays
516
+ vector1 = np.array(vectorA)
517
+ vector2 = np.array(vectorB)
518
+
519
+ # Normalize input vectors
520
+ vector1_norm = vector1 / np.linalg.norm(vector1)
521
+ vector2_norm = vector2 / np.linalg.norm(vector2)
522
+
523
+ # Check if the angle between vectors is either 0 or 180 degrees
524
+ dot_product = np.dot(vector1_norm, vector2_norm)
525
+ if np.isclose(dot_product, 1.0) or np.isclose(dot_product, -1.0):
526
+ return True
527
+ else:
528
+ # Compute bisecting vector
529
+ return False
530
+
531
+ @staticmethod
532
+ def IsParallel(vectorA, vectorB):
533
+ """
534
+ Returns True if the input vectors are parallel. Returns False otherwise.
535
+
536
+ Parameters
537
+ ----------
538
+ vectorA : list
539
+ The first input vector.
540
+ vectorB : list
541
+ The second input vector.
542
+
543
+ Returns
544
+ -------
545
+ bool
546
+ True if the input vectors are parallel. False otherwise.
547
+
548
+ """
549
+ import numpy as np
550
+
551
+
552
+ # Ensure vectors are numpy arrays
553
+ vector1 = np.array(vectorA)
554
+ vector2 = np.array(vectorB)
555
+
556
+ # Normalize input vectors
557
+ vector1_norm = vector1 / np.linalg.norm(vector1)
558
+ vector2_norm = vector2 / np.linalg.norm(vector2)
559
+
560
+ # Check if the angle between vectors is either 0 or 180 degrees
561
+ dot_product = np.dot(vector1_norm, vector2_norm)
562
+ if np.isclose(dot_product, 1.0):
563
+ return True
564
+ else:
565
+ # Compute bisecting vector
566
+ return False
567
+
568
+ @staticmethod
569
+ def Magnitude(vector, mantissa: int = 6):
570
+ """
571
+ Returns the magnitude of the input vector.
572
+
573
+ Parameters
574
+ ----------
575
+ vector : list
576
+ The input vector.
577
+ mantissa : int
578
+ The length of the desired mantissa. The default is 6.
579
+
580
+ Returns
581
+ -------
582
+ float
583
+ The magnitude of the input vector.
584
+ """
585
+
586
+ return round(np.linalg.norm(np.array(vector)), mantissa)
587
+
588
+ @staticmethod
589
+ def Multiply(vector, magnitude, tolerance=0.0001):
590
+ """
591
+ Multiplies the input vector by the input magnitude.
592
+
593
+ Parameters
594
+ ----------
595
+ vector : list
596
+ The input vector.
597
+ magnitude : float
598
+ The input magnitude.
599
+ tolerance : float, optional
600
+ the desired tolerance. The default is 0.0001.
601
+
602
+ Returns
603
+ -------
604
+ list
605
+ The created vector that multiplies the input vector by the input magnitude.
606
+
607
+ """
608
+ if abs(magnitude) < tolerance:
609
+ return [0.0] * len(vector)
610
+ scaled_vector = [component * (magnitude) for component in vector]
611
+ return scaled_vector
612
+
613
+
614
+ @staticmethod
615
+ def Normalize(vector):
616
+ """
617
+ Returns a normalized vector of the input vector. A normalized vector has the same direction as the input vector, but its magnitude is 1.
618
+
619
+ Parameters
620
+ ----------
621
+ vector : list
622
+ The input vector.
623
+
624
+ Returns
625
+ -------
626
+ list
627
+ The normalized vector.
628
+ """
629
+
630
+ return list(vector / np.linalg.norm(vector))
631
+
632
+ @staticmethod
633
+ def North():
634
+ """
635
+ Returns the vector representing the *north* direction. In Topologic, the positive YAxis direction is considered *north* ([0, 1, 0]).
636
+
637
+ Returns
638
+ -------
639
+ list
640
+ The vector representing the *north* direction.
641
+ """
642
+ return [0, 1, 0]
643
+
644
+ @staticmethod
645
+ def NorthEast():
646
+ """
647
+ Returns the vector representing the *northeast* direction. In Topologic, the positive YAxis direction is considered *north* and the positive XAxis direction is considered *east*. Therefore *northeast* is ([1, 1, 0]).
648
+
649
+ Returns
650
+ -------
651
+ list
652
+ The vector representing the *northeast* direction.
653
+ """
654
+ return [1, 1, 0]
655
+
656
+ @staticmethod
657
+ def NorthWest():
658
+ """
659
+ Returns the vector representing the *northwest* direction. In Topologic, the positive YAxis direction is considered *north* and the negative XAxis direction is considered *west*. Therefore *northwest* is ([-1, 1, 0]).
660
+
661
+ Returns
662
+ -------
663
+ list
664
+ The vector representing the *northwest* direction.
665
+ """
666
+ return [-1, 1, 0]
667
+
668
+ @staticmethod
669
+ def Reverse(vector):
670
+ """
671
+ Returns a reverse vector of the input vector. A reverse vector multiplies all components by -1.
672
+
673
+ Parameters
674
+ ----------
675
+ vector : list
676
+ The input vector.
677
+
678
+ Returns
679
+ -------
680
+ list
681
+ The normalized vector.
682
+ """
683
+ if not isinstance(vector, list):
684
+ return None
685
+ return [x*-1 for x in vector]
686
+
687
+ @staticmethod
688
+ def SetMagnitude(vector: list, magnitude: float) -> list:
689
+ """
690
+ Sets the magnitude of the input vector to the input magnitude.
691
+
692
+ Parameters
693
+ ----------
694
+ vector : list
695
+ The input vector.
696
+ magnitude : float
697
+ The desired magnitude.
698
+
699
+ Returns
700
+ -------
701
+ list
702
+ The created vector.
703
+ """
704
+ return (Vector.Multiply(vector=Vector.Normalize(vector), magnitude=magnitude))
705
+
706
+ @staticmethod
707
+ def South():
708
+ """
709
+ Returns the vector representing the *south* direction. In Topologic, the negative YAxis direction is considered *south* ([0, -1, 0]).
710
+
711
+ Returns
712
+ -------
713
+ list
714
+ The vector representing the *south* direction.
715
+ """
716
+ return [0, -1, 0]
717
+
718
+ @staticmethod
719
+ def SouthEast():
720
+ """
721
+ Returns the vector representing the *southeast* direction. In Topologic, the negative YAxis direction is considered *south* and the positive XAxis direction is considered *east*. Therefore *southeast* is ([1, -1, 0]).
722
+
723
+ Returns
724
+ -------
725
+ list
726
+ The vector representing the *southeast* direction.
727
+ """
728
+ return [1, -1, 0]
729
+
730
+ @staticmethod
731
+ def SouthWest():
732
+ """
733
+ Returns the vector representing the *southwest* direction. In Topologic, the negative YAxis direction is considered *south* and the negative XAxis direction is considered *west*. Therefore *southwest* is ([-1, -1, 0]).
734
+
735
+ Returns
736
+ -------
737
+ list
738
+ The vector representing the *southwest* direction.
739
+ """
740
+ return [-1, -1, 0]
741
+
742
+ @staticmethod
743
+ def Sum(vectors: list):
744
+ """
745
+ Returns the sum vector of the input vectors.
746
+
747
+ Parameters
748
+ ----------
749
+ vectors : list
750
+ The input list of vectors.
751
+
752
+ Returns
753
+ -------
754
+ list
755
+ The sum vector of the input list of vectors.
756
+ """
757
+ if not isinstance(vectors, list):
758
+ print("Vector.Average - Error: The input vectors parameter is not a valid list. Returning None.")
759
+ return None
760
+ vectors = [vec for vec in vectors if isinstance(vec, list)]
761
+ if len(vectors) < 1:
762
+ print("Vector.Average - Error: The input vectors parameter does not contain any valid vectors. Returning None.")
763
+ return None
764
+
765
+ dimensions = len(vectors[0])
766
+ num_vectors = len(vectors)
767
+
768
+ # Initialize a list to store the sum of each dimension
769
+ sum_dimensions = [0] * dimensions
770
+
771
+ # Calculate the sum of each dimension across all vectors
772
+ for vector in vectors:
773
+ for i in range(dimensions):
774
+ sum_dimensions[i] += vector[i]
775
+
776
+ return sum_dimensions
777
+
778
+ @staticmethod
779
+ def TransformationMatrix(vectorA, vectorB):
780
+ """
781
+ Returns the transformation matrix needed to align vectorA with vectorB.
782
+
783
+ Parameters
784
+ ----------
785
+ vectorA : list
786
+ The input vector to be transformed.
787
+ vectorB : list
788
+ The desired vector with which to align vectorA.
789
+
790
+ Returns
791
+ -------
792
+ list
793
+ Transformation matrix that follows the Blender software convention (nested list)
794
+ """
795
+ import numpy as np
796
+ from topologicpy.Matrix import Matrix
797
+
798
+ import numpy as np
799
+
800
+ def transformation_matrix(vec1, vec2, translation_vector=None):
801
+ """
802
+ Compute a 4x4 transformation matrix that aligns vec1 to vec2.
803
+
804
+ :param vec1: A 3D "source" vector
805
+ :param vec2: A 3D "destination" vector
806
+ :param translation_vector: Optional translation vector (default is None)
807
+ :return: The 4x4 transformation matrix
808
+ """
809
+ vec1 = vec1 / np.linalg.norm(vec1)
810
+ vec2 = vec2 / np.linalg.norm(vec2)
811
+ dot_product = np.dot(vec1, vec2)
812
+
813
+ if np.isclose(dot_product, 1.0):
814
+ # Vectors are parallel; return the identity matrix
815
+ return np.eye(4)
816
+ elif np.isclose(dot_product, -1.0):
817
+ # Vectors are antiparallel; reflect one of the vectors about the origin
818
+ reflection_matrix = np.eye(4)
819
+ reflection_matrix[2, 2] = -1
820
+ return reflection_matrix
821
+
822
+ cross_product = np.cross(vec1, vec2)
823
+
824
+ skew_symmetric_matrix = np.array([[0, -cross_product[2], cross_product[1], 0],
825
+ [cross_product[2], 0, -cross_product[0], 0],
826
+ [-cross_product[1], cross_product[0], 0, 0],
827
+ [0, 0, 0, 1]])
828
+
829
+ rotation_matrix = np.eye(4) + skew_symmetric_matrix + \
830
+ np.dot(skew_symmetric_matrix, skew_symmetric_matrix) * \
831
+ (1 / (1 + dot_product))
832
+
833
+ if translation_vector is not None:
834
+ translation_matrix = np.eye(4)
835
+ translation_matrix[:3, 3] = translation_vector
836
+ transformation_matrix = np.dot(translation_matrix, rotation_matrix)
837
+ else:
838
+ transformation_matrix = rotation_matrix
839
+
840
+ return transformation_matrix
841
+
842
+
843
+ tran_mat = transformation_matrix(vectorA, vectorB)
844
+
845
+ return [list(tran_mat[0]), list(tran_mat[1]), list(tran_mat[2]), list(tran_mat[3])]
846
+
847
+ @staticmethod
848
+ def Up():
849
+ """
850
+ Returns the vector representing the up direction. In Topologic, the positive ZAxis direction is considered "up" ([0, 0, 1]).
851
+
852
+ Returns
853
+ -------
854
+ list
855
+ The vector representing the "up" direction.
856
+ """
857
+ return [0, 0, 1]
858
+
859
+ @staticmethod
860
+ def West():
861
+ """
862
+ Returns the vector representing the *west* direction. In Topologic, the negative XAxis direction is considered *west* ([-1, 0, 0]).
863
+
864
+ Returns
865
+ -------
866
+ list
867
+ The vector representing the *west* direction.
868
+ """
869
+ return [-1, 0, 0]
870
+
871
+ @staticmethod
872
+ def XAxis():
873
+ """
874
+ Returns the vector representing the XAxis ([1, 0, 0])
875
+
876
+ Returns
877
+ -------
878
+ list
879
+ The vector representing the XAxis.
880
+ """
881
+ return [1, 0, 0]
882
+
883
+ @staticmethod
884
+ def YAxis():
885
+ """
886
+ Returns the vector representing the YAxis ([0, 1, 0])
887
+
888
+ Returns
889
+ -------
890
+ list
891
+ The vector representing the YAxis.
892
+ """
893
+ return [0, 1, 0]
894
+
895
+ @staticmethod
896
+ def ZAxis():
897
+ """
898
+ Returns the vector representing the ZAxis ([0, 0, 1])
899
+
900
+ Returns
901
+ -------
902
+ list
903
+ The vector representing the ZAxis.
904
+ """
905
+ return [0, 0, 1]