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.
- topologicpy/Aperture.py +72 -72
- topologicpy/Cell.py +2169 -2169
- topologicpy/CellComplex.py +1137 -1137
- topologicpy/Cluster.py +1288 -1280
- topologicpy/Color.py +423 -423
- topologicpy/Context.py +79 -79
- topologicpy/DGL.py +3213 -3240
- topologicpy/Dictionary.py +698 -698
- topologicpy/Edge.py +1187 -1187
- topologicpy/EnergyModel.py +1180 -1152
- topologicpy/Face.py +2141 -2141
- topologicpy/Graph.py +7768 -7768
- topologicpy/Grid.py +353 -353
- topologicpy/Helper.py +507 -507
- topologicpy/Honeybee.py +461 -461
- topologicpy/Matrix.py +271 -271
- topologicpy/Neo4j.py +521 -521
- topologicpy/Plotly.py +2 -2
- topologicpy/Polyskel.py +541 -541
- topologicpy/Shell.py +1768 -1768
- topologicpy/Speckle.py +508 -508
- topologicpy/Topology.py +7060 -7002
- topologicpy/Vector.py +905 -905
- topologicpy/Vertex.py +1585 -1585
- topologicpy/Wire.py +3050 -3050
- topologicpy/__init__.py +22 -38
- topologicpy/version.py +1 -0
- {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/LICENSE +661 -704
- topologicpy-6.0.0.dist-info/METADATA +751 -0
- topologicpy-6.0.0.dist-info/RECORD +32 -0
- topologicpy/bin/linux/topologic/__init__.py +0 -2
- topologicpy/bin/linux/topologic/libTKBO-6bdf205d.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKBRep-2960a069.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKBool-c44b74bd.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKFillet-9a670ba0.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKG2d-8f31849e.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKG3d-4c6bce57.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKMath-72572fa8.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKMesh-2a060427.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKOffset-6cab68ff.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKPrim-eb1262b3.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic/libgcc_s-32c1665e.so.1 +0 -0
- topologicpy/bin/linux/topologic/libstdc++-672d7b41.so.6.0.30 +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-310-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-311-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-38-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic/topologic.cpython-39-x86_64-linux-gnu.so +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBO-6bdf205d.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBRep-2960a069.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKBool-c44b74bd.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKFillet-9a670ba0.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKG2d-8f31849e.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKG3d-4c6bce57.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKMath-72572fa8.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKMesh-2a060427.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKOffset-6cab68ff.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKPrim-eb1262b3.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
- topologicpy/bin/linux/topologic.libs/libgcc_s-32c1665e.so.1 +0 -0
- topologicpy/bin/linux/topologic.libs/libstdc++-672d7b41.so.6.0.30 +0 -0
- topologicpy/bin/macos/topologic/__init__.py +0 -2
- topologicpy/bin/windows/topologic/TKBO-f6b191de.dll +0 -0
- topologicpy/bin/windows/topologic/TKBRep-e56a600e.dll +0 -0
- topologicpy/bin/windows/topologic/TKBool-7b8d47ae.dll +0 -0
- topologicpy/bin/windows/topologic/TKFillet-0ddbf0a8.dll +0 -0
- topologicpy/bin/windows/topologic/TKG2d-2e2dee3d.dll +0 -0
- topologicpy/bin/windows/topologic/TKG3d-6674513d.dll +0 -0
- topologicpy/bin/windows/topologic/TKGeomAlgo-d240e370.dll +0 -0
- topologicpy/bin/windows/topologic/TKGeomBase-df87aba5.dll +0 -0
- topologicpy/bin/windows/topologic/TKMath-45bd625a.dll +0 -0
- topologicpy/bin/windows/topologic/TKMesh-d6e826b1.dll +0 -0
- topologicpy/bin/windows/topologic/TKOffset-79b9cc94.dll +0 -0
- topologicpy/bin/windows/topologic/TKPrim-aa430a86.dll +0 -0
- topologicpy/bin/windows/topologic/TKShHealing-bb48be89.dll +0 -0
- topologicpy/bin/windows/topologic/TKTopAlgo-7d0d1e22.dll +0 -0
- topologicpy/bin/windows/topologic/TKernel-08c8cfbb.dll +0 -0
- topologicpy/bin/windows/topologic/__init__.py +0 -2
- topologicpy/bin/windows/topologic/topologic.cp310-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp311-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp38-win_amd64.pyd +0 -0
- topologicpy/bin/windows/topologic/topologic.cp39-win_amd64.pyd +0 -0
- topologicpy-0.5.9.dist-info/METADATA +0 -86
- topologicpy-0.5.9.dist-info/RECORD +0 -91
- {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/WHEEL +0 -0
- {topologicpy-0.5.9.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
|
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
|
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
|
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
|
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
|
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]
|