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/Face.py CHANGED
@@ -1,2141 +1,2141 @@
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 topologic
18
- from topologicpy.Vector import Vector
19
- from topologicpy.Wire import Wire
20
- from topologicpy.Topology import Topology
21
- import math
22
- import os
23
- import warnings
24
-
25
- try:
26
- import numpy as np
27
- except:
28
- print("Face - Installing required numpy library.")
29
- try:
30
- os.system("pip install numpy")
31
- except:
32
- os.system("pip install numpy --user")
33
- try:
34
- import numpy as np
35
- print("Face - numpy library installed correctly.")
36
- except:
37
- warnings.warn("Face - Error: Could not import numpy.")
38
-
39
- class Face(Topology):
40
- @staticmethod
41
- def AddInternalBoundaries(face: topologic.Face, wires: list) -> topologic.Face:
42
- """
43
- Adds internal boundaries (closed wires) to the input face. Internal boundaries are considered holes in the input face.
44
-
45
- Parameters
46
- ----------
47
- face : topologic.Face
48
- The input face.
49
- wires : list
50
- The input list of internal boundaries (closed wires).
51
-
52
- Returns
53
- -------
54
- topologic.Face
55
- The created face with internal boundaries added to it.
56
-
57
- """
58
-
59
- if not isinstance(face, topologic.Face):
60
- print("Face.AddInternalBoundaries - Error: The input face parameter is not a valid topologic face. Returning None.")
61
- return None
62
- if not isinstance(wires, list):
63
- print("Face.AddInternalBoundaries - Warning: The input wires parameter is not a valid list. Returning the input face.")
64
- return face
65
- wireList = [w for w in wires if isinstance(w, topologic.Wire)]
66
- if len(wireList) < 1:
67
- print("Face.AddInternalBoundaries - Warning: The input wires parameter does not contain any valid wires. Returning the input face.")
68
- return face
69
- faceeb = face.ExternalBoundary()
70
- faceibList = []
71
- _ = face.InternalBoundaries(faceibList)
72
- for wire in wires:
73
- faceibList.append(wire)
74
- return topologic.Face.ByExternalInternalBoundaries(faceeb, faceibList)
75
-
76
- @staticmethod
77
- def AddInternalBoundariesCluster(face: topologic.Face, cluster: topologic.Cluster) -> topologic.Face:
78
- """
79
- Adds the input cluster of internal boundaries (closed wires) to the input face. Internal boundaries are considered holes in the input face.
80
-
81
- Parameters
82
- ----------
83
- face : topologic.Face
84
- The input face.
85
- cluster : topologic.Cluster
86
- The input cluster of internal boundaries (topologic wires).
87
-
88
- Returns
89
- -------
90
- topologic.Face
91
- The created face with internal boundaries added to it.
92
-
93
- """
94
- if not isinstance(face, topologic.Face):
95
- print("Face.AddInternalBoundariesCluster - Warning: The input cluster parameter is not a valid cluster. Returning None.")
96
- return None
97
- if not cluster:
98
- return face
99
- if not isinstance(cluster, topologic.Cluster):
100
- return face
101
- wires = []
102
- _ = cluster.Wires(None, wires)
103
- return Face.AddInternalBoundaries(face, wires)
104
-
105
- @staticmethod
106
- def Angle(faceA: topologic.Face, faceB: topologic.Face, mantissa: int = 6) -> float:
107
- """
108
- Returns the angle in degrees between the two input faces.
109
-
110
- Parameters
111
- ----------
112
- faceA : topologic.Face
113
- The first input face.
114
- faceB : topologic.Face
115
- The second input face.
116
- mantissa : int , optional
117
- The desired length of the mantissa. The default is 6.
118
-
119
- Returns
120
- -------
121
- float
122
- The angle in degrees between the two input faces.
123
-
124
- """
125
- from topologicpy.Vector import Vector
126
- if not isinstance(faceA, topologic.Face):
127
- print("Face.Angle - Warning: The input faceA parameter is not a valid topologic face. Returning None.")
128
- return None
129
- if not isinstance(faceB, topologic.Face):
130
- print("Face.Angle - Warning: The input faceB parameter is not a valid topologic face. Returning None.")
131
- return None
132
- dirA = Face.NormalAtParameters(faceA, 0.5, 0.5, "xyz", 3)
133
- dirB = Face.NormalAtParameters(faceB, 0.5, 0.5, "xyz", 3)
134
- return round((Vector.Angle(dirA, dirB)), mantissa)
135
-
136
- @staticmethod
137
- def Area(face: topologic.Face, mantissa: int = 6) -> float:
138
- """
139
- Returns the area of the input face.
140
-
141
- Parameters
142
- ----------
143
- face : topologic.Face
144
- The input face.
145
- mantissa : int , optional
146
- The desired length of the mantissa. The default is 6.
147
-
148
- Returns
149
- -------
150
- float
151
- The area of the input face.
152
-
153
- """
154
- if not isinstance(face, topologic.Face):
155
- print("Face.Area - Warning: The input face parameter is not a valid topologic face. Returning None.")
156
- return None
157
- area = None
158
- try:
159
- area = round(topologic.FaceUtility.Area(face), mantissa)
160
- except:
161
- area = None
162
- return area
163
-
164
- @staticmethod
165
- def BoundingRectangle(topology: topologic.Topology, optimize: int = 0, tolerance: float = 0.0001) -> topologic.Face:
166
- """
167
- Returns a face representing a bounding rectangle of the input topology. The returned face contains a dictionary with key "zrot" that represents rotations around the Z axis. If applied the resulting face will become axis-aligned.
168
-
169
- Parameters
170
- ----------
171
- topology : topologic.Topology
172
- The input topology.
173
- optimize : int , optional
174
- If set to an integer from 1 (low optimization) to 10 (high optimization), the method will attempt to optimize the bounding rectangle so that it reduces its surface area. The default is 0 which will result in an axis-aligned bounding rectangle. The default is 0.
175
- tolerance : float , optional
176
- The desired tolerance. The default is 0.0001.
177
-
178
- Returns
179
- -------
180
- topologic.Face
181
- The bounding rectangle of the input topology.
182
-
183
- """
184
- from topologicpy.Wire import Wire
185
- from topologicpy.Face import Face
186
- from topologicpy.Cluster import Cluster
187
- from topologicpy.Topology import Topology
188
- from topologicpy.Dictionary import Dictionary
189
- def bb(topology):
190
- vertices = []
191
- _ = topology.Vertices(None, vertices)
192
- x = []
193
- y = []
194
- for aVertex in vertices:
195
- x.append(aVertex.X())
196
- y.append(aVertex.Y())
197
- minX = min(x)
198
- minY = min(y)
199
- maxX = max(x)
200
- maxY = max(y)
201
- return [minX, minY, maxX, maxY]
202
-
203
- if not isinstance(topology, topologic.Topology):
204
- print("Face.BoundingRectangle - Warning: The input topology parameter is not a valid topologic topology. Returning None.")
205
- return None
206
- vertices = Topology.SubTopologies(topology, subTopologyType="vertex")
207
- topology = Cluster.ByTopologies(vertices)
208
- boundingBox = bb(topology)
209
- minX = boundingBox[0]
210
- minY = boundingBox[1]
211
- maxX = boundingBox[2]
212
- maxY = boundingBox[3]
213
- w = abs(maxX - minX)
214
- l = abs(maxY - minY)
215
- best_area = l*w
216
- orig_area = best_area
217
- best_z = 0
218
- best_bb = boundingBox
219
- origin = Topology.Centroid(topology)
220
- optimize = min(max(optimize, 0), 10)
221
- if optimize > 0:
222
- factor = (round(((11 - optimize)/30 + 0.57), 2))
223
- flag = False
224
- for n in range(10,0,-1):
225
- if flag:
226
- break
227
- za = n
228
- zb = 90+n
229
- zc = n
230
- for z in range(za,zb,zc):
231
- if flag:
232
- break
233
- t = Topology.Rotate(topology, origin=origin, axis=[0, 0, 1], angle=z)
234
- minX, minY, maxX, maxY = bb(t)
235
- w = abs(maxX - minX)
236
- l = abs(maxY - minY)
237
- area = l*w
238
- if area < orig_area*factor:
239
- best_area = area
240
- best_z = z
241
- best_bb = [minX, minY, maxX, maxY]
242
- flag = True
243
- break
244
- if area < best_area:
245
- best_area = area
246
- best_z = z
247
- best_bb = [minX, minY, maxX, maxY]
248
-
249
- else:
250
- best_bb = boundingBox
251
-
252
- minX, minY, maxX, maxY = best_bb
253
- vb1 = topologic.Vertex.ByCoordinates(minX, minY, 0)
254
- vb2 = topologic.Vertex.ByCoordinates(maxX, minY, 0)
255
- vb3 = topologic.Vertex.ByCoordinates(maxX, maxY, 0)
256
- vb4 = topologic.Vertex.ByCoordinates(minX, maxY, 0)
257
-
258
- baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], close=True)
259
- baseFace = Face.ByWire(baseWire, tolerance=tolerance)
260
- baseFace = Topology.Rotate(baseFace, origin=origin, axis=[0, 0, 1], angle=-best_z)
261
- dictionary = Dictionary.ByKeysValues(["zrot"], [best_z])
262
- baseFace = Topology.SetDictionary(baseFace, dictionary)
263
- return baseFace
264
-
265
- @staticmethod
266
- def ByEdges(edges: list, tolerance : float = 0.0001) -> topologic.Face:
267
- """
268
- Creates a face from the input list of edges.
269
-
270
- Parameters
271
- ----------
272
- edges : list
273
- The input list of edges.
274
- tolerance : float , optional
275
- The desired tolerance. The default is 0.0001.
276
-
277
- Returns
278
- -------
279
- face : topologic.Face
280
- The created face.
281
-
282
- """
283
- from topologicpy.Wire import Wire
284
- if not isinstance(edges, list):
285
- print("Face.ByEdges - Error: The input edges parameter is not a valid list. Returning None.")
286
- return None
287
- edges = [e for e in edges if isinstance(e, topologic.Edge)]
288
- if len(edges) < 1:
289
- print("Face.ByEdges - Error: The input edges parameter does not contain any valid edges. Returning None.")
290
- return None
291
- wire = Wire.ByEdges(edges, tolerance=tolerance)
292
- if not isinstance(wire, topologic.Wire):
293
- print("Face.ByEdges - Error: Could not create the required wire. Returning None.")
294
- return None
295
- return Face.ByWire(wire, tolerance=tolerance)
296
-
297
- @staticmethod
298
- def ByEdgesCluster(cluster: topologic.Cluster, tolerance: float = 0.0001) -> topologic.Face:
299
- """
300
- Creates a face from the input cluster of edges.
301
-
302
- Parameters
303
- ----------
304
- cluster : topologic.Cluster
305
- The input cluster of edges.
306
- tolerance : float , optional
307
- The desired tolerance. The default is 0.0001.
308
-
309
- Returns
310
- -------
311
- face : topologic.Face
312
- The created face.
313
-
314
- """
315
- from topologicpy.Cluster import Cluster
316
- if not isinstance(cluster, topologic.Cluster):
317
- print("Face.ByEdgesCluster - Warning: The input cluster parameter is not a valid topologic cluster. Returning None.")
318
- return None
319
- edges = Cluster.Edges(cluster)
320
- if len(edges) < 1:
321
- print("Face.ByEdgesCluster - Warning: The input cluster parameter does not contain any valid edges. Returning None.")
322
- return None
323
- return Face.ByEdges(edges, tolerance=tolerance)
324
-
325
- @staticmethod
326
- def ByOffset(face: topologic.Face, offset: float = 1.0, miter: bool = False,
327
- miterThreshold: float = None, offsetKey: str = None,
328
- miterThresholdKey: str = None, step: bool = True, tolerance: float = 0.0001) -> topologic.Face:
329
- """
330
- Creates an offset face from the input face.
331
-
332
- Parameters
333
- ----------
334
- face : topologic.Face
335
- The input face.
336
- offset : float , optional
337
- The desired offset distance. The default is 1.0.
338
- miter : bool , optional
339
- if set to True, the corners will be mitered. The default is False.
340
- miterThreshold : float , optional
341
- The distance beyond which a miter should be added. The default is None which means the miter threshold is set to the offset distance multiplied by the square root of 2.
342
- offsetKey : str , optional
343
- If specified, the dictionary of the edges will be queried for this key to sepcify the desired offset. The default is None.
344
- miterThresholdKey : str , optional
345
- If specified, the dictionary of the vertices will be queried for this key to sepcify the desired miter threshold distance. The default is None.
346
- step : bool , optional
347
- If set to True, The transition between collinear edges with different offsets will be a step. Otherwise, it will be a continous edge. The default is True.
348
- tolerance : float , optional
349
- The desired tolerance. The default is 0.0001.
350
-
351
- Returns
352
- -------
353
- topologic.Face
354
- The created face.
355
-
356
- """
357
- from topologicpy.Wire import Wire
358
- if not isinstance(face, topologic.Face):
359
- print("Face.ByOffset - Warning: The input face parameter is not a valid toplogic face. Returning None.")
360
- return None
361
- eb = Face.Wire(face)
362
- internal_boundaries = Face.InternalBoundaries(face)
363
- offset_external_boundary = Wire.ByOffset(wire=eb, offset=offset, miter=miter, miterThreshold=miterThreshold, offsetKey=offsetKey, miterThresholdKey=miterThresholdKey, step=step)
364
- offset_internal_boundaries = []
365
- for internal_boundary in internal_boundaries:
366
- offset_internal_boundaries.append(Wire.ByOffset(wire=internal_boundary, offset=offset, miter=miter, miterThreshold=miterThreshold, offsetKey=offsetKey, miterThresholdKey=miterThresholdKey, step=step))
367
- return Face.ByWires(offset_external_boundary, offset_internal_boundaries, tolerance=tolerance)
368
-
369
- @staticmethod
370
- def ByShell(shell: topologic.Shell, origin: topologic.Vertex = None, angTolerance: float = 0.1, tolerance: float = 0.0001)-> topologic.Face:
371
- """
372
- Creates a face by merging the faces of the input shell.
373
-
374
- Parameters
375
- ----------
376
- shell : topologic.Shell
377
- The input shell.
378
- angTolerance : float , optional
379
- The desired angular tolerance. The default is 0.1.
380
- tolerance : float , optional
381
- The desired tolerance. The default is 0.0001.
382
-
383
- Returns
384
- -------
385
- topologic.Face
386
- The created face.
387
-
388
- """
389
- from topologicpy.Vertex import Vertex
390
- from topologicpy.Wire import Wire
391
- from topologicpy.Shell import Shell
392
- from topologicpy.Cluster import Cluster
393
- from topologicpy.Topology import Topology
394
- from topologicpy.Dictionary import Dictionary
395
- from topologicpy.Helper import Helper
396
-
397
- def planarizeList(wireList):
398
- returnList = []
399
- for aWire in wireList:
400
- returnList.append(Wire.Planarize(aWire))
401
- return returnList
402
-
403
- if not isinstance(shell, topologic.Shell):
404
- print("Face.ByShell - Error: The input shell parameter is not a valid toplogic shell. Returning None.")
405
- return None
406
-
407
- if origin == None:
408
- origin = Topology.Centroid(shell)
409
- if not isinstance(origin, topologic.Vertex):
410
- print("Face.ByShell - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
411
- return None
412
-
413
- # Try the simple method first
414
- face = None
415
- ext_boundary = Wire.RemoveCollinearEdges(Shell.ExternalBoundary(shell))
416
- if isinstance(ext_boundary, topologic.Wire):
417
- face = Face.ByWire(ext_boundary)
418
- elif isinstance(ext_boundary, topologic.Cluster):
419
- wires = Topology.Wires(ext_boundary)
420
- faces = [Face.ByWire(w) for w in wires]
421
- areas = [Face.Area(f) for f in faces]
422
- wires = Helper.Sort(wires, areas, reverseFlags=[True])
423
- face = Face.ByWires(wires[0], wires[1:])
424
-
425
- if isinstance(face, topologic.Face):
426
- return face
427
- world_origin = Vertex.Origin()
428
- planar_shell = Shell.Planarize(shell)
429
- normal = Face.Normal(Topology.Faces(planar_shell)[0])
430
- planar_shell = Topology.Flatten(planar_shell, origin=origin, direction=normal)
431
- vertices = Shell.Vertices(planar_shell)
432
- new_vertices = []
433
- for v in vertices:
434
- x, y, z = Vertex.Coordinates(v)
435
- new_v = Vertex.ByCoordinates(x,y,0)
436
- new_vertices.append(new_v)
437
- planar_shell = Topology.SelfMerge(Topology.ReplaceVertices(planar_shell, verticesA=vertices, verticesB=new_vertices), tolerance=tolerance)
438
- ext_boundary = Shell.ExternalBoundary(planar_shell, tolerance=tolerance)
439
- ext_boundary = Topology.RemoveCollinearEdges(ext_boundary, angTolerance)
440
- if not isinstance(ext_boundary, topologic.Topology):
441
- print("Face.ByShell - Error: Could not derive the external boundary of the input shell parameter. Returning None.")
442
- return None
443
-
444
- if isinstance(ext_boundary, topologic.Wire):
445
- if not Topology.IsPlanar(ext_boundary, tolerance=tolerance):
446
- ext_boundary = Wire.Planarize(ext_boundary, origin=origin, tolerance=tolerance)
447
- ext_boundary = Topology.RemoveCollinearEdges(ext_boundary, angTolerance)
448
- try:
449
- face = Face.ByWire(ext_boundary)
450
- face = Topology.Unflatten(face, origin=origin, direction=normal)
451
- return face
452
- except:
453
- print("Face.ByShell - Error: The operation failed. Returning None.")
454
- return None
455
- elif isinstance(ext_boundary, topologic.Cluster): # The shell has holes.
456
- wires = []
457
- _ = ext_boundary.Wires(None, wires)
458
- faces = []
459
- areas = []
460
- for wire in wires:
461
- aFace = Face.ByWire(wire, tolerance=tolerance)
462
- if not isinstance(aFace, topologic.Face):
463
- print("Face.ByShell - Error: The operation failed. Returning None.")
464
- return None
465
- anArea = abs(Face.Area(aFace))
466
- faces.append(aFace)
467
- areas.append(anArea)
468
- max_index = areas.index(max(areas))
469
- ext_boundary = faces[max_index]
470
- int_boundaries = list(set(faces) - set([ext_boundary]))
471
- int_wires = []
472
- for int_boundary in int_boundaries:
473
- temp_wires = []
474
- _ = int_boundary.Wires(None, temp_wires)
475
- int_wires.append(Topology.RemoveCollinearEdges(temp_wires[0], angTolerance))
476
- temp_wires = []
477
- _ = ext_boundary.Wires(None, temp_wires)
478
- ext_wire = Topology.RemoveCollinearEdges(temp_wires[0], angTolerance)
479
- face = Face.ByWires(ext_wire, int_wires)
480
-
481
- face = Topology.Unflatten(face, origin=origin, direction=normal)
482
- return face
483
- else:
484
- return None
485
-
486
- @staticmethod
487
- def ByVertices(vertices: list, tolerance: float = 0.0001) -> topologic.Face:
488
-
489
- """
490
- Creates a face from the input list of vertices.
491
-
492
- Parameters
493
- ----------
494
- vertices : list
495
- The input list of vertices.
496
- tolerance : float , optional
497
- The desired tolerance. The default is 0.0001.
498
-
499
- Returns
500
- -------
501
- topologic.Face
502
- The created face.
503
-
504
- """
505
- from topologicpy.Topology import Topology
506
- from topologicpy.Wire import Wire
507
-
508
- if not isinstance(vertices, list):
509
- return None
510
- vertexList = [x for x in vertices if isinstance(x, topologic.Vertex)]
511
- if len(vertexList) < 3:
512
- return None
513
-
514
- w = Wire.ByVertices(vertexList, tolerance=tolerance)
515
- f = Face.ByWire(w, tolerance=tolerance)
516
- return f
517
-
518
- @staticmethod
519
- def ByVerticesCluster(cluster: topologic.Cluster, tolerance: float = 0.0001) -> topologic.Face:
520
- """
521
- Creates a face from the input cluster of vertices.
522
-
523
- Parameters
524
- ----------
525
- cluster : topologic.Cluster
526
- The input cluster of vertices.
527
- tolerance : float , optional
528
- The desired tolerance. The default is 0.0001.
529
-
530
- Returns
531
- -------
532
- topologic.Face
533
- The crearted face.
534
-
535
- """
536
- from topologicpy.Cluster import Cluster
537
- if not isinstance(cluster, topologic.Cluster):
538
- return None
539
- vertices = Cluster.Vertices(cluster)
540
- return Face.ByVertices(vertices, tolerance=tolerance)
541
-
542
- @staticmethod
543
- def ByWire(wire: topologic.Wire, tolerance: float = 0.0001, silent=False) -> topologic.Face:
544
- """
545
- Creates a face from the input closed wire.
546
-
547
- Parameters
548
- ----------
549
- wire : topologic.Wire
550
- The input wire.
551
- tolerance : float , optional
552
- The desired tolerance. The default is 0.0001.
553
- silent : bool , optional
554
- If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
555
-
556
- Returns
557
- -------
558
- topologic.Face or list
559
- The created face. If the wire is non-planar, the method will attempt to triangulate the wire and return a list of faces.
560
-
561
- """
562
- from topologicpy.Vertex import Vertex
563
- from topologicpy.Wire import Wire
564
- from topologicpy.Shell import Shell
565
- from topologicpy.Cluster import Cluster
566
- from topologicpy.Topology import Topology
567
- from topologicpy.Dictionary import Dictionary
568
- import random
569
-
570
- def triangulateWire(wire):
571
- wire = Topology.RemoveCollinearEdges(wire)
572
- vertices = Topology.Vertices(wire)
573
- shell = Shell.Delaunay(vertices)
574
- if isinstance(shell, topologic.Topology):
575
- return Topology.Faces(shell)
576
- else:
577
- return []
578
- if not isinstance(wire, topologic.Wire):
579
- if not silent:
580
- print("Face.ByWire - Error: The input wire parameter is not a valid topologic wire. Returning None.")
581
- return None
582
- if not Wire.IsClosed(wire):
583
- if not silent:
584
- print("Face.ByWire - Error: The input wire parameter is not a closed topologic wire. Returning None.")
585
- return None
586
-
587
- edges = Wire.Edges(wire)
588
- wire = Topology.SelfMerge(Cluster.ByTopologies(edges), tolerance=tolerance)
589
- vertices = Topology.Vertices(wire)
590
- fList = []
591
- if isinstance(wire, topologic.Wire):
592
- try:
593
- fList = topologic.Face.ByExternalBoundary(wire)
594
- except:
595
- if not silent:
596
- print("Face.ByWire - Warning: Could not create face by external boundary. Trying other methods.")
597
- if len(vertices) > 3:
598
- fList = triangulateWire(wire)
599
- else:
600
- fList = []
601
-
602
- if not isinstance(fList, list):
603
- fList = [fList]
604
-
605
- returnList = []
606
- for f in fList:
607
- if Face.Area(f) < 0:
608
- wire = Face.ExternalBoundary(f)
609
- wire = Wire.Invert(wire)
610
- try:
611
- f = topologic.Face.ByExternalBoundary(wire)
612
- returnList.append(f)
613
- except:
614
- pass
615
- else:
616
- returnList.append(f)
617
- if len(returnList) == 0:
618
- if not silent:
619
- print("Face.ByWire - Error: Could not build a face from the input wire parameter. Returning None.")
620
- return None
621
- elif len(returnList) == 1:
622
- return returnList[0]
623
- else:
624
- if not silent:
625
- print("Face.ByWire - Warning: Could not build a single face from the input wire parameter. Returning a list of faces.")
626
- return returnList
627
-
628
- @staticmethod
629
- def ByWires(externalBoundary: topologic.Wire, internalBoundaries: list = [], tolerance: float = 0.0001, silent: bool = False) -> topologic.Face:
630
- """
631
- Creates a face from the input external boundary (closed wire) and the input list of internal boundaries (closed wires).
632
-
633
- Parameters
634
- ----------
635
- externalBoundary : topologic.Wire
636
- The input external boundary.
637
- internalBoundaries : list , optional
638
- The input list of internal boundaries (closed wires). The default is an empty list.
639
- tolerance : float , optional
640
- The desired tolerance. The default is 0.0001.
641
- silent : bool , optional
642
- If set to False, error messages are printed. Otherwise, they are not. The default is False.
643
-
644
- Returns
645
- -------
646
- topologic.Face
647
- The created face.
648
-
649
- """
650
- if not isinstance(externalBoundary, topologic.Wire):
651
- if not silent:
652
- print("Face.ByWires - Error: The input externalBoundary parameter is not a valid topologic wire. Returning None.")
653
- return None
654
- if not Wire.IsClosed(externalBoundary):
655
- if not silent:
656
- print("Face.ByWires - Error: The input externalBoundary parameter is not a closed topologic wire. Returning None.")
657
- return None
658
- ibList = [x for x in internalBoundaries if isinstance(x, topologic.Wire) and Wire.IsClosed(x)]
659
- face = None
660
- try:
661
- face = topologic.Face.ByExternalInternalBoundaries(externalBoundary, ibList, tolerance)
662
- except:
663
- if not silent:
664
- print("Face.ByWires - Error: The operation failed. Returning None.")
665
- face = None
666
- return face
667
-
668
-
669
- @staticmethod
670
- def ByWiresCluster(externalBoundary: topologic.Wire, internalBoundariesCluster: topologic.Cluster = None, tolerance: float = 0.0001, silent: bool = False) -> topologic.Face:
671
- """
672
- Creates a face from the input external boundary (closed wire) and the input cluster of internal boundaries (closed wires).
673
-
674
- Parameters
675
- ----------
676
- externalBoundary : topologic.Wire
677
- The input external boundary (closed wire).
678
- internalBoundariesCluster : topologic.Cluster
679
- The input cluster of internal boundaries (closed wires). The default is None.
680
- tolerance : float , optional
681
- The desired tolerance. The default is 0.0001.
682
- silent : bool , optional
683
- If set to False, error messages are printed. Otherwise, they are not. The default is False.
684
-
685
- Returns
686
- -------
687
- topologic.Face
688
- The created face.
689
-
690
- """
691
- from topologicpy.Wire import Wire
692
- from topologicpy.Cluster import Cluster
693
- if not isinstance(externalBoundary, topologic.Wire):
694
- if not silent:
695
- print("Face.ByWiresCluster - Error: The input externalBoundary parameter is not a valid topologic wire. Returning None.")
696
- return None
697
- if not Wire.IsClosed(externalBoundary):
698
- if not silent:
699
- print("Face.ByWiresCluster - Error: The input externalBoundary parameter is not a closed topologic wire. Returning None.")
700
- return None
701
- if not internalBoundariesCluster:
702
- internalBoundaries = []
703
- elif not isinstance(internalBoundariesCluster, topologic.Cluster):
704
- if not silent:
705
- print("Face.ByWiresCluster - Error: The input internalBoundariesCluster parameter is not a valid topologic cluster. Returning None.")
706
- return None
707
- else:
708
- internalBoundaries = Cluster.Wires(internalBoundariesCluster)
709
- return Face.ByWires(externalBoundary, internalBoundaries, tolerance=tolerance, silent=silent)
710
-
711
- @staticmethod
712
- def NorthArrow(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 16, direction: list = [0, 0, 1], northAngle: float = 0.0,
713
- placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
714
- """
715
- Creates a north arrow.
716
-
717
- Parameters
718
- ----------
719
- origin : topologic.Vertex, optional
720
- The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
721
- radius : float , optional
722
- The radius of the circle. The default is 1.
723
- sides : int , optional
724
- The number of sides of the circle. The default is 16.
725
- direction : list , optional
726
- The vector representing the up direction of the circle. The default is [0, 0, 1].
727
- northAngle : float , optional
728
- The angular offset in degrees from the positive Y axis direction. The angle is measured in a counter-clockwise fashion where 0 is positive Y, 90 is negative X, 180 is negative Y, and 270 is positive X.
729
- placement : str , optional
730
- The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
731
- tolerance : float , optional
732
- The desired tolerance. The default is 0.0001.
733
-
734
- Returns
735
- -------
736
- topologic.Face
737
- The created circle.
738
-
739
- """
740
- from topologicpy.Topology import Topology
741
- from topologicpy.Vertex import Vertex
742
- if not origin:
743
- origin = Vertex.Origin()
744
-
745
- c = Face.Circle(origin=origin, radius=radius, sides=sides, direction=[0, 0, 1], placement="center", tolerance=tolerance)
746
- r = Face.Rectangle(origin=origin, width=radius*0.01,length=radius*1.2, placement="lowerleft")
747
- r = Topology.Translate(r, -0.005*radius,0,0)
748
- arrow = Topology.Difference(c, r, tolerance=tolerance)
749
- arrow = Topology.Rotate(arrow, origin=Vertex.Origin(), axis=[0, 0, 1], angle=northAngle)
750
- if placement.lower() == "lowerleft":
751
- arrow = Topology.Translate(arrow, radius, radius, 0)
752
- elif placement.lower() == "upperleft":
753
- arrow = Topology.Translate(arrow, radius, -radius, 0)
754
- elif placement.lower() == "lowerright":
755
- arrow = Topology.Translate(arrow, -radius, radius, 0)
756
- elif placement.lower() == "upperright":
757
- arrow = Topology.Translate(arrow, -radius, -radius, 0)
758
- arrow = Topology.Place(arrow, originA=Vertex.Origin(), originB=origin)
759
- arrow = Topology.Orient(arrow, orign=origin, direction=direction)
760
- return arrow
761
-
762
- @staticmethod
763
- def Circle(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 16, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1],
764
- placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
765
- """
766
- Creates a circle.
767
-
768
- Parameters
769
- ----------
770
- origin : topologic.Vertex, optional
771
- The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
772
- radius : float , optional
773
- The radius of the circle. The default is 1.
774
- sides : int , optional
775
- The number of sides of the circle. The default is 16.
776
- fromAngle : float , optional
777
- The angle in degrees from which to start creating the arc of the circle. The default is 0.
778
- toAngle : float , optional
779
- The angle in degrees at which to end creating the arc of the circle. The default is 360.
780
- direction : list , optional
781
- The vector representing the up direction of the circle. The default is [0, 0, 1].
782
- placement : str , optional
783
- The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
784
- tolerance : float , optional
785
- The desired tolerance. The default is 0.0001.
786
-
787
- Returns
788
- -------
789
- topologic.Face
790
- The created circle.
791
-
792
- """
793
- from topologicpy.Wire import Wire
794
- wire = Wire.Circle(origin=origin, radius=radius, sides=sides, fromAngle=fromAngle, toAngle=toAngle, close=True, direction=direction, placement=placement, tolerance=tolerance)
795
- if not isinstance(wire, topologic.Wire):
796
- return None
797
- return Face.ByWire(wire, tolerance=tolerance)
798
-
799
- @staticmethod
800
- def Compactness(face: topologic.Face, mantissa: int = 6) -> float:
801
- """
802
- Returns the compactness measure of the input face. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
803
-
804
- Parameters
805
- ----------
806
- face : topologic.Face
807
- The input face.
808
- mantissa : int , optional
809
- The desired length of the mantissa. The default is 6.
810
-
811
- Returns
812
- -------
813
- float
814
- The compactness measure of the input face.
815
-
816
- """
817
- exb = face.ExternalBoundary()
818
- edges = []
819
- _ = exb.Edges(None, edges)
820
- perimeter = 0.0
821
- for anEdge in edges:
822
- perimeter = perimeter + abs(topologic.EdgeUtility.Length(anEdge))
823
- area = abs(Face.Area(face))
824
- compactness = 0
825
- #From https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
826
-
827
- if area <= 0:
828
- return None
829
- if perimeter <= 0:
830
- return None
831
- compactness = (math.pi*(2*math.sqrt(area/math.pi)))/perimeter
832
- return round(compactness, mantissa)
833
-
834
- @staticmethod
835
- def CompassAngle(face: topologic.Face, north: list = None, mantissa: int = 6) -> float:
836
- """
837
- Returns the horizontal compass angle in degrees between the normal vector of the input face and the input vector. The angle is measured in counter-clockwise fashion. Only the first two elements of the vectors are considered.
838
-
839
- Parameters
840
- ----------
841
- face : topologic.Face
842
- The input face.
843
- north : list , optional
844
- The second vector representing the north direction. The default is the positive YAxis ([0,1,0]).
845
- mantissa : int, optional
846
- The length of the desired mantissa. The default is 4.
847
- tolerance : float , optional
848
- The desired tolerance. The default is 0.0001.
849
-
850
- Returns
851
- -------
852
- float
853
- The horizontal compass angle in degrees between the direction of the face and the second input vector.
854
-
855
- """
856
- from topologicpy.Vector import Vector
857
- if not isinstance(face, topologic.Face):
858
- return None
859
- if not north:
860
- north = Vector.North()
861
- dirA = Face.NormalAtParameters(face,mantissa=mantissa)
862
- return Vector.CompassAngle(vectorA=dirA, vectorB=north, mantissa=mantissa)
863
-
864
- @staticmethod
865
- def Edges(face: topologic.Face) -> list:
866
- """
867
- Returns the edges of the input face.
868
-
869
- Parameters
870
- ----------
871
- face : topologic.Face
872
- The input face.
873
-
874
- Returns
875
- -------
876
- list
877
- The list of edges.
878
-
879
- """
880
- if not isinstance(face, topologic.Face):
881
- return None
882
- edges = []
883
- _ = face.Edges(None, edges)
884
- return edges
885
-
886
- @staticmethod
887
- def Einstein(origin: topologic.Vertex = None, radius: float = 0.5, direction: list = [0, 0, 1],
888
- placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
889
- """
890
- Creates an aperiodic monotile, also called an 'einstein' tile (meaning one tile in German, not the name of the famous physist). See https://arxiv.org/abs/2303.10798
891
-
892
- Parameters
893
- ----------
894
- origin : topologic.Vertex , optional
895
- The location of the origin of the tile. The default is None which results in the tiles first vertex being placed at (0, 0, 0).
896
- radius : float , optional
897
- The radius of the hexagon determining the size of the tile. The default is 0.5.
898
- direction : list , optional
899
- The vector representing the up direction of the ellipse. The default is [0, 0, 1].
900
- placement : str , optional
901
- The description of the placement of the origin of the hexagon determining the location of the tile. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
902
- tolerance : float , optional
903
- The desired tolerance. The default is 0.0001.
904
-
905
- Returns
906
- --------
907
- topologic.Face
908
- The created Einstein tile.
909
-
910
- """
911
- from topologicpy.Wire import Wire
912
-
913
- wire = Wire.Einstein(origin=origin, radius=radius, direction=direction, placement=placement)
914
- if not isinstance(wire, topologic.Wire):
915
- print("Face.Einstein - Error: Could not create base wire for the Einstein tile. Returning None.")
916
- return None
917
- return Face.ByWire(wire, tolerance=tolerance)
918
-
919
- @staticmethod
920
- def ExteriorAngles(face: topologic.Face, includeInternalBoundaries=False, mantissa: int = 6) -> list:
921
- """
922
- Returns the exterior angles of the input face in degrees. The face must be planar.
923
-
924
- Parameters
925
- ----------
926
- face : topologic.Face
927
- The input face.
928
- includeInternalBoundaries : bool , optional
929
- If set to True and if the face has internal boundaries (holes), the returned list will be a nested list where the first list is the list
930
- of exterior angles of the external boundary and the second list will contain lists of the exterior angles of each of the
931
- internal boundaries (holes). For example: [[270,270,270,270], [[270,270,270,270],[300,300,300]]]. If not, the returned list will be
932
- a simple list of interior angles of the external boundary. For example: [270,270,270,270]. Please note that that the interior angles of the
933
- internal boundaries are considered to be those interior to the original face. Thus, they are exterior to the internal boundary.
934
- mantissa : int , optional
935
- The desired length of the mantissa. The default is 6.
936
- Returns
937
- -------
938
- list
939
- The list of exterior angles.
940
- """
941
-
942
- from topologicpy.Wire import Wire
943
-
944
- if not isinstance(face, topologic.Face):
945
- print("Face.ExteriorAngles - Error: The input face parameter is not a valid face. Returning None.")
946
- return None
947
- eb = Face.ExternalBoundary(face)
948
- return_list = Wire.ExteriorAngles(eb, mantissa=mantissa)
949
- if includeInternalBoundaries:
950
- internal_boundaries = Face.InternalBoundaries(face)
951
- ib_i_a_list = []
952
- if len(internal_boundaries) > 0:
953
- for ib in internal_boundaries:
954
- ib_interior_angles = Wire.InteriorAngles(ib, mantissa=mantissa)
955
- ib_i_a_list.append(ib_interior_angles)
956
- if len(ib_i_a_list) > 0:
957
- return_list = [return_list]+[ib_i_a_list]
958
- return return_list
959
-
960
- @staticmethod
961
- def ExternalBoundary(face: topologic.Face) -> topologic.Wire:
962
- """
963
- Returns the external boundary (closed wire) of the input face.
964
-
965
- Parameters
966
- ----------
967
- face : topologic.Face
968
- The input face.
969
-
970
- Returns
971
- -------
972
- topologic.Wire
973
- The external boundary of the input face.
974
-
975
- """
976
- return face.ExternalBoundary()
977
-
978
- @staticmethod
979
- def FacingToward(face: topologic.Face, direction: list = [0,0,-1], asVertex: bool = False, tolerance: float = 0.0001) -> bool:
980
- """
981
- Returns True if the input face is facing toward the input direction.
982
-
983
- Parameters
984
- ----------
985
- face : topologic.Face
986
- The input face.
987
- direction : list , optional
988
- The input direction. The default is [0,0,-1].
989
- asVertex : bool , optional
990
- If set to True, the direction is treated as an actual vertex in 3D space. The default is False.
991
- tolerance : float , optional
992
- The desired tolerance. The default is 0.0001.
993
-
994
- Returns
995
- -------
996
- bool
997
- True if the face is facing toward the direction. False otherwise.
998
-
999
- """
1000
- faceNormal = topologic.FaceUtility.NormalAtParameters(face,0.5, 0.5)
1001
- faceCenter = topologic.FaceUtility.VertexAtParameters(face,0.5,0.5)
1002
- cList = [faceCenter.X(), faceCenter.Y(), faceCenter.Z()]
1003
- try:
1004
- vList = [direction.X(), direction.Y(), direction.Z()]
1005
- except:
1006
- try:
1007
- vList = [direction[0], direction[1], direction[2]]
1008
- except:
1009
- raise Exception("Face.FacingToward - Error: Could not get the vector from the input direction")
1010
- if asVertex:
1011
- dV = [vList[0]-cList[0], vList[1]-cList[1], vList[2]-cList[2]]
1012
- else:
1013
- dV = vList
1014
- uV = Vector.Normalize(dV)
1015
- dot = sum([i*j for (i, j) in zip(uV, faceNormal)])
1016
- if dot < tolerance:
1017
- return False
1018
- return True
1019
-
1020
- @staticmethod
1021
- def Harmonize(face: topologic.Face, tolerance: float = 0.0001) -> topologic.Face:
1022
- """
1023
- Returns a harmonized version of the input face such that the *u* and *v* origins are always in the upperleft corner.
1024
-
1025
- Parameters
1026
- ----------
1027
- face : topologic.Face
1028
- The input face.
1029
- tolerance : float , optional
1030
- The desired tolerance. The default is 0.0001.
1031
-
1032
- Returns
1033
- -------
1034
- topologic.Face
1035
- The harmonized face.
1036
-
1037
- """
1038
- from topologicpy.Vertex import Vertex
1039
- from topologicpy.Wire import Wire
1040
- from topologicpy.Topology import Topology
1041
- from topologicpy.Dictionary import Dictionary
1042
-
1043
- if not isinstance(face, topologic.Face):
1044
- print("Face.Harmonize - Error: The input face parameter is not a valid face. Returning None.")
1045
- return None
1046
- normal = Face.Normal(face)
1047
- origin = Topology.Centroid(face)
1048
- flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1049
- world_origin = Vertex.Origin()
1050
- vertices = Wire.Vertices(Face.ExternalBoundary(flatFace))
1051
- harmonizedEB = Wire.ByVertices(vertices)
1052
- internalBoundaries = Face.InternalBoundaries(flatFace)
1053
- harmonizedIB = []
1054
- for ib in internalBoundaries:
1055
- ibVertices = Wire.Vertices(ib)
1056
- harmonizedIB.append(Wire.ByVertices(ibVertices))
1057
- harmonizedFace = Face.ByWires(harmonizedEB, harmonizedIB, tolerance=tolerance)
1058
- harmonizedFace = Topology.Unflatten(harmonizedFace, origin=origin, direction=normal)
1059
- return harmonizedFace
1060
-
1061
- @staticmethod
1062
- def InteriorAngles(face: topologic.Face, includeInternalBoundaries: bool = False, mantissa: int = 6) -> list:
1063
- """
1064
- Returns the interior angles of the input face in degrees. The face must be planar.
1065
-
1066
- Parameters
1067
- ----------
1068
- face : topologic.Face
1069
- The input face.
1070
- includeInternalBoundaries : bool , optional
1071
- If set to True and if the face has internal boundaries (holes), the returned list will be a nested list where the first list is the list
1072
- of interior angles of the external boundary and the second list will contain lists of the interior angles of each of the
1073
- internal boundaries (holes). For example: [[90,90,90,90], [[90,90,90,90],[60,60,60]]]. If not, the returned list will be
1074
- a simple list of interior angles of the external boundary. For example: [90,90,90,90]. Please note that that the interior angles of the
1075
- internal boundaries are considered to be those interior to the original face. Thus, they are exterior to the internal boundary.
1076
- mantissa : int , optional
1077
- The desired length of the mantissa. The default is 6.
1078
- Returns
1079
- -------
1080
- list
1081
- The list of interior angles.
1082
- """
1083
-
1084
- from topologicpy.Wire import Wire
1085
-
1086
- if not isinstance(face, topologic.Face):
1087
- print("Face.InteriorAngles - Error: The input face parameter is not a valid face. Returning None.")
1088
- return None
1089
- eb = Face.ExternalBoundary(face)
1090
- return_list = Wire.InteriorAngles(eb, mantissa=mantissa)
1091
- if includeInternalBoundaries:
1092
- internal_boundaries = Face.InternalBoundaries(face)
1093
- ib_i_a_list = []
1094
- if len(internal_boundaries) > 0:
1095
- for ib in internal_boundaries:
1096
- ib_interior_angles = Wire.ExteriorAngles(ib, mantissa=mantissa)
1097
- ib_i_a_list.append(ib_interior_angles)
1098
- if len(ib_i_a_list) > 0:
1099
- return_list = [return_list]+[ib_i_a_list]
1100
- return return_list
1101
-
1102
- @staticmethod
1103
- def InternalBoundaries(face: topologic.Face) -> list:
1104
- """
1105
- Returns the internal boundaries (closed wires) of the input face.
1106
-
1107
- Parameters
1108
- ----------
1109
- face : topologic.Face
1110
- The input face.
1111
-
1112
- Returns
1113
- -------
1114
- list
1115
- The list of internal boundaries (closed wires).
1116
-
1117
- """
1118
- if not isinstance(face, topologic.Face):
1119
- return None
1120
- wires = []
1121
- _ = face.InternalBoundaries(wires)
1122
- return list(wires)
1123
-
1124
- @staticmethod
1125
- def InternalVertex(face: topologic.Face, tolerance: float = 0.0001) -> topologic.Vertex:
1126
- """
1127
- Creates a vertex guaranteed to be inside the input face.
1128
-
1129
- Parameters
1130
- ----------
1131
- face : topologic.Face
1132
- The input face.
1133
- tolerance : float , optional
1134
- The desired tolerance. The default is 0.0001.
1135
-
1136
- Returns
1137
- -------
1138
- topologic.Vertex
1139
- The created vertex.
1140
-
1141
- """
1142
- from topologicpy.Vertex import Vertex
1143
- from topologicpy.Topology import Topology
1144
- if not isinstance(face, topologic.Face):
1145
- return None
1146
- v = Topology.Centroid(face)
1147
- if Vertex.IsInternal(v, face, tolerance=tolerance):
1148
- return v
1149
- l = [0.4,0.6,0.3,0.7,0.2,0.8,0.1,0.9]
1150
- for u in l:
1151
- for v in l:
1152
- v = Face.VertexByParameters(face, u, v)
1153
- if Vertex.IsInternal(v, face, tolerance=tolerance):
1154
- return v
1155
- v = topologic.FaceUtility.InternalVertex(face, tolerance)
1156
- return v
1157
-
1158
- @staticmethod
1159
- def Invert(face: topologic.Face, tolerance: float = 0.0001) -> topologic.Face:
1160
- """
1161
- Creates a face that is an inverse (mirror) of the input face.
1162
-
1163
- Parameters
1164
- ----------
1165
- face : topologic.Face
1166
- The input face.
1167
- tolerance : float , optional
1168
- The desired tolerance. The default is 0.0001.
1169
-
1170
- Returns
1171
- -------
1172
- topologic.Face
1173
- The inverted face.
1174
-
1175
- """
1176
- from topologicpy.Wire import Wire
1177
-
1178
- if not isinstance(face, topologic.Face):
1179
- return None
1180
- eb = Face.ExternalBoundary(face)
1181
- vertices = Wire.Vertices(eb)
1182
- vertices.reverse()
1183
- inverted_wire = Wire.ByVertices(vertices)
1184
- internal_boundaries = Face.InternalBoundaries(face)
1185
- if not internal_boundaries:
1186
- inverted_face = Face.ByWire(inverted_wire, tolerance=tolerance)
1187
- else:
1188
- inverted_face = Face.ByWires(inverted_wire, internal_boundaries, tolerance=tolerance)
1189
- return inverted_face
1190
-
1191
- @staticmethod
1192
- def IsCoplanar(faceA: topologic.Face, faceB: topologic.Face, tolerance: float = 0.0001) -> bool:
1193
- """
1194
- Returns True if the two input faces are coplanar. Returns False otherwise.
1195
-
1196
- Parameters
1197
- ----------
1198
- faceA : topologic.Face
1199
- The first input face.
1200
- faceB : topologic.Face
1201
- The second input face
1202
- tolerance : float , optional
1203
- The desired tolerance. The deafault is 0.0001.
1204
-
1205
- Raises
1206
- ------
1207
- Exception
1208
- Raises an exception if the angle between the two input faces cannot be determined.
1209
-
1210
- Returns
1211
- -------
1212
- bool
1213
- True if the two input faces are coplanar. False otherwise.
1214
-
1215
- """
1216
- if not isinstance(faceA, topologic.Face):
1217
- print("Face.IsInide - Error: The input faceA parameter is not a valid topologic face. Returning None.")
1218
- return None
1219
- if not isinstance(faceB, topologic.Face):
1220
- print("Face.IsInide - Error: The input faceB parameter is not a valid topologic face. Returning None.")
1221
- return None
1222
- dirA = Face.NormalAtParameters(faceA, 0.5, 0.5, "xyz", 3)
1223
- dirB = Face.NormalAtParameters(faceB, 0.5, 0.5, "xyz", 3)
1224
- return Vector.IsCollinear(dirA, dirB)
1225
-
1226
- @staticmethod
1227
- def MedialAxis(face: topologic.Face, resolution: int = 0, externalVertices: bool = False, internalVertices: bool = False, toLeavesOnly: bool = False, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Wire:
1228
- """
1229
- Returns a wire representing an approximation of the medial axis of the input topology. See https://en.wikipedia.org/wiki/Medial_axis.
1230
-
1231
- Parameters
1232
- ----------
1233
- face : topologic.Face
1234
- The input face.
1235
- resolution : int , optional
1236
- The desired resolution of the solution (range is 0: standard resolution to 10: high resolution). This determines the density of the sampling along each edge. The default is 0.
1237
- externalVertices : bool , optional
1238
- If set to True, the external vertices of the face will be connected to the nearest vertex on the medial axis. The default is False.
1239
- internalVertices : bool , optional
1240
- If set to True, the internal vertices of the face will be connected to the nearest vertex on the medial axis. The default is False.
1241
- toLeavesOnly : bool , optional
1242
- If set to True, the vertices of the face will be connected to the nearest vertex on the medial axis only if this vertex is a leaf (end point). Otherwise, it will connect to any nearest vertex. The default is False.
1243
- angTolerance : float , optional
1244
- The desired angular tolerance in degrees for removing collinear edges. The default is 0.1.
1245
- tolerance : float , optional
1246
- The desired tolerance. The default is 0.0001.
1247
-
1248
- Returns
1249
- -------
1250
- topologic.Wire
1251
- The medial axis of the input face.
1252
-
1253
- """
1254
- from topologicpy.Vertex import Vertex
1255
- from topologicpy.Edge import Edge
1256
- from topologicpy.Wire import Wire
1257
- from topologicpy.Shell import Shell
1258
- from topologicpy.Cluster import Cluster
1259
- from topologicpy.Topology import Topology
1260
- from topologicpy.Dictionary import Dictionary
1261
-
1262
- def touchesEdge(vertex,edges, tolerance=0.0001):
1263
- if not isinstance(vertex, topologic.Vertex):
1264
- return False
1265
- for edge in edges:
1266
- u = Edge.ParameterAtVertex(edge, vertex, mantissa=6)
1267
- if not u:
1268
- continue
1269
- if 0<u<1:
1270
- return True
1271
- return False
1272
-
1273
- # Flatten the input face
1274
- origin = Topology.Centroid(face)
1275
- normal = Face.Normal(face)
1276
- flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1277
-
1278
- # Create a Vertex at the world's origin (0, 0, 0)
1279
- world_origin = Vertex.Origin()
1280
-
1281
- faceEdges = Face.Edges(flatFace)
1282
- vertices = []
1283
- resolution = 10 - resolution
1284
- resolution = min(max(resolution, 1), 10)
1285
- for e in faceEdges:
1286
- for n in range(resolution, 100, resolution):
1287
- vertices.append(Edge.VertexByParameter(e,n*0.01))
1288
-
1289
- voronoi = Shell.Voronoi(vertices=vertices, face=flatFace)
1290
- voronoiEdges = Shell.Edges(voronoi)
1291
-
1292
- medialAxisEdges = []
1293
- for e in voronoiEdges:
1294
- sv = Edge.StartVertex(e)
1295
- ev = Edge.EndVertex(e)
1296
- svTouchesEdge = touchesEdge(sv, faceEdges, tolerance=tolerance)
1297
- evTouchesEdge = touchesEdge(ev, faceEdges, tolerance=tolerance)
1298
- if not svTouchesEdge and not evTouchesEdge:
1299
- medialAxisEdges.append(e)
1300
-
1301
- extBoundary = Face.ExternalBoundary(flatFace)
1302
- extVertices = Wire.Vertices(extBoundary)
1303
-
1304
- intBoundaries = Face.InternalBoundaries(flatFace)
1305
- intVertices = []
1306
- for ib in intBoundaries:
1307
- intVertices = intVertices+Wire.Vertices(ib)
1308
-
1309
- theVertices = []
1310
- if internalVertices:
1311
- theVertices = theVertices+intVertices
1312
- if externalVertices:
1313
- theVertices = theVertices+extVertices
1314
-
1315
- tempWire = Topology.SelfMerge(Cluster.ByTopologies(medialAxisEdges), tolerance=tolerance)
1316
- if isinstance(tempWire, topologic.Wire) and angTolerance > 0:
1317
- tempWire = Wire.RemoveCollinearEdges(tempWire, angTolerance=angTolerance)
1318
- medialAxisEdges = Wire.Edges(tempWire)
1319
- for v in theVertices:
1320
- nv = Vertex.NearestVertex(v, tempWire, useKDTree=False)
1321
-
1322
- if isinstance(nv, topologic.Vertex):
1323
- if toLeavesOnly:
1324
- adjVertices = Topology.AdjacentTopologies(nv, tempWire)
1325
- if len(adjVertices) < 2:
1326
- medialAxisEdges.append(Edge.ByVertices([nv, v], tolerance=tolerance))
1327
- else:
1328
- medialAxisEdges.append(Edge.ByVertices([nv, v], tolerance=tolerance))
1329
- medialAxis = Topology.SelfMerge(Cluster.ByTopologies(medialAxisEdges), tolerance=tolerance)
1330
- if isinstance(medialAxis, topologic.Wire) and angTolerance > 0:
1331
- medialAxis = Topology.RemoveCollinearEdges(medialAxis, angTolerance=angTolerance)
1332
- medialAxis = Topology.Unflatten(medialAxis, origin=origin,direction=normal)
1333
- return medialAxis
1334
-
1335
- @staticmethod
1336
- def Normal(face: topologic.Face, outputType: str = "xyz", mantissa: int = 6) -> list:
1337
- """
1338
- Returns the normal vector to the input face. A normal vector of a face is a vector perpendicular to it.
1339
-
1340
- Parameters
1341
- ----------
1342
- face : topologic.Face
1343
- The input face.
1344
- outputType : string , optional
1345
- The string defining the desired output. This can be any subset or permutation of "xyz". It is case insensitive. The default is "xyz".
1346
- mantissa : int , optional
1347
- The desired length of the mantissa. The default is 6.
1348
-
1349
- Returns
1350
- -------
1351
- list
1352
- The normal vector to the input face. This is computed at the approximate center of the face.
1353
-
1354
- """
1355
- return Face.NormalAtParameters(face, u=0.5, v=0.5, outputType=outputType, mantissa=mantissa)
1356
-
1357
- @staticmethod
1358
- def NormalAtParameters(face: topologic.Face, u: float = 0.5, v: float = 0.5, outputType: str = "xyz", mantissa: int = 6) -> list:
1359
- """
1360
- Returns the normal vector to the input face. A normal vector of a face is a vector perpendicular to it.
1361
-
1362
- Parameters
1363
- ----------
1364
- face : topologic.Face
1365
- The input face.
1366
- u : float , optional
1367
- The *u* parameter at which to compute the normal to the input face. The default is 0.5.
1368
- v : float , optional
1369
- The *v* parameter at which to compute the normal to the input face. The default is 0.5.
1370
- outputType : string , optional
1371
- The string defining the desired output. This can be any subset or permutation of "xyz". It is case insensitive. The default is "xyz".
1372
- mantissa : int , optional
1373
- The desired length of the mantissa. The default is 6.
1374
-
1375
- Returns
1376
- -------
1377
- list
1378
- The normal vector to the input face.
1379
-
1380
- """
1381
- returnResult = []
1382
- try:
1383
- coords = topologic.FaceUtility.NormalAtParameters(face, u, v)
1384
- x = round(coords[0], mantissa)
1385
- y = round(coords[1], mantissa)
1386
- z = round(coords[2], mantissa)
1387
- outputType = list(outputType.lower())
1388
- for axis in outputType:
1389
- if axis == "x":
1390
- returnResult.append(x)
1391
- elif axis == "y":
1392
- returnResult.append(y)
1393
- elif axis == "z":
1394
- returnResult.append(z)
1395
- except:
1396
- returnResult = None
1397
- return returnResult
1398
-
1399
- @staticmethod
1400
- def NormalEdge(face: topologic.Face, length: float = 1.0, tolerance: float = 0.0001, silent: bool = False) -> topologic.Edge:
1401
- """
1402
- Returns the normal vector to the input face as an edge with the desired input length. A normal vector of a face is a vector perpendicular to it.
1403
-
1404
- Parameters
1405
- ----------
1406
- face : topologic.Face
1407
- The input face.
1408
- length : float , optional
1409
- The desired length of the normal edge. The default is 1.
1410
- tolerance : float , optional
1411
- The desired tolerance. The default is 0.0001.
1412
-
1413
- Returns
1414
- -------
1415
- topologic.Edge
1416
- The created normal edge to the input face. This is computed at the approximate center of the face.
1417
-
1418
- """
1419
- from topologicpy.Edge import Edge
1420
-
1421
- if not isinstance(face, topologic.Face):
1422
- if not silent:
1423
- print("Face.NormalEdge - Error: The input face parameter is not a valid face. Retuning None.")
1424
- return None
1425
- if length < tolerance:
1426
- if not silent:
1427
- print("Face.NormalEdge - Error: The input length parameter is less than the input tolerance. Retuning None.")
1428
- return None
1429
- iv = Face.InternalVertex(face)
1430
- u, v = Face.VertexParameters(face, iv)
1431
- vec = Face.NormalAtParameters(face, u=u, v=v)
1432
- ev = Topology.TranslateByDirectionDistance(iv, vec, length)
1433
- return Edge.ByVertices([iv, ev], tolerance=tolerance, silent=silent)
1434
-
1435
- @staticmethod
1436
- def NormalEdgeAtParameters(face: topologic.Face, u: float = 0.5, v: float = 0.5, length: float = 1.0, tolerance: float = 0.0001) -> topologic.Edge:
1437
- """
1438
- Returns the normal vector to the input face as an edge with the desired input length. A normal vector of a face is a vector perpendicular to it.
1439
-
1440
- Parameters
1441
- ----------
1442
- face : topologic.Face
1443
- The input face.
1444
- u : float , optional
1445
- The *u* parameter at which to compute the normal to the input face. The default is 0.5.
1446
- v : float , optional
1447
- The *v* parameter at which to compute the normal to the input face. The default is 0.5.
1448
- length : float , optional
1449
- The desired length of the normal edge. The default is 1.
1450
- tolerance : float , optional
1451
- The desired tolerance. The default is 0.0001.
1452
-
1453
- Returns
1454
- -------
1455
- topologic.Edge
1456
- The created normal edge to the input face. This is computed at the approximate center of the face.
1457
-
1458
- """
1459
- from topologicpy.Edge import Edge
1460
- from topologicpy.Topology import Topology
1461
- if not isinstance(face, topologic.Face):
1462
- return None
1463
- sv = Face.VertexByParameters(face=face, u=u, v=v)
1464
- vec = Face.NormalAtParameters(face, u=u, v=v)
1465
- ev = Topology.TranslateByDirectionDistance(sv, vec, length)
1466
- return Edge.ByVertices([sv, ev], tolerance=tolerance, silent=True)
1467
-
1468
- @staticmethod
1469
- def PlaneEquation(face: topologic.Face, mantissa: int = 6) -> dict:
1470
- """
1471
- Returns the a, b, c, d coefficients of the plane equation of the input face. The input face is assumed to be planar.
1472
-
1473
- Parameters
1474
- ----------
1475
- face : topologic.Face
1476
- The input face.
1477
-
1478
- Returns
1479
- -------
1480
- dict
1481
- The dictionary containing the coefficients of the plane equation. The keys of the dictionary are: ["a", "b", "c", "d"].
1482
-
1483
- """
1484
- from topologicpy.Topology import Topology
1485
- from topologicpy.Vertex import Vertex
1486
- import random
1487
- import time
1488
-
1489
- if not isinstance(face, topologic.Face):
1490
- print("Face.PlaneEquation - Error: The input face is not a valid topologic face. Returning None.")
1491
- return None
1492
- vertices = Topology.Vertices(face)
1493
- if len(vertices) < 3:
1494
- print("Face.PlaneEquation - Error: The input face has less than 3 vertices. Returning None.")
1495
- return None
1496
- return Vertex.PlaneEquation(vertices, mantissa=mantissa)
1497
-
1498
- @staticmethod
1499
- def Planarize(face: topologic.Face, origin: topologic.Vertex = None,
1500
- tolerance: float = 0.0001) -> topologic.Face:
1501
- """
1502
- Planarizes the input face such that its center of mass is located at the input origin and its normal is pointed in the input direction.
1503
-
1504
- Parameters
1505
- ----------
1506
- face : topologic.Face
1507
- The input face.
1508
- origin : topologic.Vertex , optional
1509
- The desired vertex to use as the origin of the plane to project the face unto. If set to None, the centroidof the input face is used. The default is None.
1510
- tolerance : float , optional
1511
- The desired tolerance. The default is 0.0001.
1512
-
1513
- Returns
1514
- -------
1515
- topologic.Face
1516
- The planarized face.
1517
-
1518
- """
1519
-
1520
- from topologicpy.Wire import Wire
1521
- from topologicpy.Topology import Topology
1522
-
1523
- if not isinstance(face, topologic.Face):
1524
- return None
1525
- if not isinstance(origin, topologic.Vertex):
1526
- origin = Topology.Centroid(face)
1527
- eb = Face.ExternalBoundary(face)
1528
- plan_eb = Wire.Planarize(eb, origin=origin)
1529
- ib_list = Face.InternalBoundaries(face)
1530
- plan_ib_list = []
1531
- for ib in ib_list:
1532
- plan_ib_list.append(Wire.Planarize(ib, origin=origin))
1533
- plan_face = Face.ByWires(plan_eb, plan_ib_list)
1534
- return plan_face
1535
-
1536
- @staticmethod
1537
- def Project(faceA: topologic.Face, faceB: topologic.Face, direction : list = None,
1538
- mantissa: int = 6, tolerance: float = 0.0001) -> topologic.Face:
1539
- """
1540
- Creates a projection of the first input face unto the second input face.
1541
-
1542
- Parameters
1543
- ----------
1544
- faceA : topologic.Face
1545
- The face to be projected.
1546
- faceB : topologic.Face
1547
- The face unto which the first input face will be projected.
1548
- direction : list, optional
1549
- The vector direction of the projection. If None, the reverse vector of the receiving face normal will be used. The default is None.
1550
- mantissa : int , optional
1551
- The desired length of the mantissa. The default is 6.
1552
- tolerance : float , optional
1553
- The desired tolerance. The default is 0.0001.
1554
-
1555
- Returns
1556
- -------
1557
- topologic.Face
1558
- The projected Face.
1559
-
1560
- """
1561
-
1562
- from topologicpy.Wire import Wire
1563
-
1564
- if not faceA:
1565
- return None
1566
- if not isinstance(faceA, topologic.Face):
1567
- return None
1568
- if not faceB:
1569
- return None
1570
- if not isinstance(faceB, topologic.Face):
1571
- return None
1572
-
1573
- eb = faceA.ExternalBoundary()
1574
- ib_list = []
1575
- _ = faceA.InternalBoundaries(ib_list)
1576
- p_eb = Wire.Project(wire=eb, face = faceB, direction=direction, mantissa=mantissa, tolerance=tolerance)
1577
- p_ib_list = []
1578
- for ib in ib_list:
1579
- temp_ib = Wire.Project(wire=ib, face = faceB, direction=direction, mantissa=mantissa, tolerance=tolerance)
1580
- if temp_ib:
1581
- p_ib_list.append(temp_ib)
1582
- return Face.ByWires(p_eb, p_ib_list, tolerance=tolerance)
1583
-
1584
- @staticmethod
1585
- def RectangleByPlaneEquation(origin: topologic.Vertex = None, width: float = 1.0, length: float = 1.0, placement: str = "center", equation: dict = None, tolerance: float = 0.0001) -> topologic.Face:
1586
- from topologicpy.Vertex import Vertex
1587
- # Extract coefficients of the plane equation
1588
- a = equation['a']
1589
- b = equation['b']
1590
- c = equation['c']
1591
- d = equation['d']
1592
-
1593
- # Calculate the normal vector of the plane
1594
- direction = np.array([a, b, c], dtype=float)
1595
- direction /= np.linalg.norm(direction)
1596
- direction = [x for x in direction]
1597
-
1598
- return Face.Rectangle(origin=origin, width=width, length=length, direction = direction, placement=placement, tolerance=tolerance)
1599
-
1600
- @staticmethod
1601
- def Rectangle(origin: topologic.Vertex = None, width: float = 1.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1602
- """
1603
- Creates a rectangle.
1604
-
1605
- Parameters
1606
- ----------
1607
- origin : topologic.Vertex, optional
1608
- The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0).
1609
- width : float , optional
1610
- The width of the rectangle. The default is 1.0.
1611
- length : float , optional
1612
- The length of the rectangle. The default is 1.0.
1613
- direction : list , optional
1614
- The vector representing the up direction of the rectangle. The default is [0, 0, 1].
1615
- placement : str , optional
1616
- The description of the placement of the origin of the rectangle. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
1617
- tolerance : float , optional
1618
- The desired tolerance. The default is 0.0001.
1619
-
1620
- Returns
1621
- -------
1622
- topologic.Face
1623
- The created face.
1624
-
1625
- """
1626
- from topologicpy.Wire import Wire
1627
-
1628
- wire = Wire.Rectangle(origin=origin, width=width, length=length, direction=direction, placement=placement, tolerance=tolerance)
1629
- if not isinstance(wire, topologic.Wire):
1630
- print("Face.Rectangle - Error: Could not create the base wire for the rectangle. Returning None.")
1631
- return None
1632
- return Face.ByWire(wire, tolerance=tolerance)
1633
-
1634
- @staticmethod
1635
- def RemoveCollinearEdges(face: topologic.Face, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Wire:
1636
- """
1637
- Removes any collinear edges in the input face.
1638
-
1639
- Parameters
1640
- ----------
1641
- face : topologic.Face
1642
- The input face.
1643
- angTolerance : float , optional
1644
- The desired angular tolerance. The default is 0.1.
1645
- tolerance : float , optional
1646
- The desired tolerance. The default is 0.0001.
1647
-
1648
- Returns
1649
- -------
1650
- topologic.Face
1651
- The created face without any collinear edges.
1652
-
1653
- """
1654
- from topologicpy.Wire import Wire
1655
-
1656
- if not isinstance(face, topologic.Face):
1657
- print("Face.RemoveCollinearEdges - Error: The input face parameter is not a valid face. Returning None.")
1658
- return None
1659
- eb = Wire.RemoveCollinearEdges(Face.Wire(face), angTolerance=angTolerance, tolerance=tolerance)
1660
- ib = [Wire.RemoveCollinearEdges(w, angTolerance=angTolerance, tolerance=tolerance) for w in Face.InternalBoundaries(face)]
1661
- return Face.ByWires(eb, ib)
1662
-
1663
- @staticmethod
1664
- def Skeleton(face, tolerance=0.001):
1665
- """
1666
- Creates a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
1667
- This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
1668
-
1669
- Parameters
1670
- ----------
1671
- face : topologic.Face
1672
- The input face.
1673
- tolerance : float , optional
1674
- The desired tolerance. The default is 0.001. (This is set to a larger number than the usual 0.0001 as it was found to work better)
1675
-
1676
- Returns
1677
- -------
1678
- topologic.Wire
1679
- The created straight skeleton.
1680
-
1681
- """
1682
- from topologicpy.Wire import Wire
1683
- if not isinstance(face, topologic.Face):
1684
- print("Face.Skeleton - Error: The input face is not a valid topologic face. Retruning None.")
1685
- return None
1686
- return Wire.Skeleton(face, tolerance=tolerance)
1687
-
1688
- @staticmethod
1689
- def Square(origin: topologic.Vertex = None, size: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1690
- """
1691
- Creates a square.
1692
-
1693
- Parameters
1694
- ----------
1695
- origin : topologic.Vertex , optional
1696
- The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0).
1697
- size : float , optional
1698
- The size of the square. The default is 1.0.
1699
- direction : list , optional
1700
- The vector representing the up direction of the square. The default is [0, 0, 1].
1701
- placement : str , optional
1702
- The description of the placement of the origin of the square. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
1703
- tolerance : float , optional
1704
- The desired tolerance. The default is 0.0001.
1705
-
1706
- Returns
1707
- -------
1708
- topologic.Face
1709
- The created square.
1710
-
1711
- """
1712
- return Face.Rectangle(origin=origin, width=size, length=size, direction=direction, placement=placement, tolerance=tolerance)
1713
-
1714
- @staticmethod
1715
- def Star(origin: topologic.Vertex = None, radiusA: float = 1.0, radiusB: float = 0.4, rays: int = 5, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1716
- """
1717
- Creates a star.
1718
-
1719
- Parameters
1720
- ----------
1721
- origin : topologic.Vertex, optional
1722
- The location of the origin of the star. The default is None which results in the star being placed at (0, 0, 0).
1723
- radiusA : float , optional
1724
- The outer radius of the star. The default is 1.0.
1725
- radiusB : float , optional
1726
- The outer radius of the star. The default is 0.4.
1727
- rays : int , optional
1728
- The number of star rays. The default is 5.
1729
- direction : list , optional
1730
- The vector representing the up direction of the star. The default is [0, 0, 1].
1731
- placement : str , optional
1732
- The description of the placement of the origin of the star. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
1733
- tolerance : float , optional
1734
- The desired tolerance. The default is 0.0001.
1735
-
1736
- Returns
1737
- -------
1738
- topologic.Face
1739
- The created face.
1740
-
1741
- """
1742
- from topologicpy.Wire import Wire
1743
- wire = Wire.Star(origin=origin, radiusA=radiusA, radiusB=radiusB, rays=rays, direction=direction, placement=placement, tolerance=tolerance)
1744
- if not isinstance(wire, topologic.Wire):
1745
- print("Face.Rectangle - Error: Could not create the base wire for the star. Returning None.")
1746
- return None
1747
- return Face.ByWire(wire, tolerance=tolerance)
1748
-
1749
- @staticmethod
1750
- def Trapezoid(origin: topologic.Vertex = None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1751
- """
1752
- Creates a trapezoid.
1753
-
1754
- Parameters
1755
- ----------
1756
- origin : topologic.Vertex, optional
1757
- The location of the origin of the trapezoid. The default is None which results in the trapezoid being placed at (0, 0, 0).
1758
- widthA : float , optional
1759
- The width of the bottom edge of the trapezoid. The default is 1.0.
1760
- widthB : float , optional
1761
- The width of the top edge of the trapezoid. The default is 0.75.
1762
- offsetA : float , optional
1763
- The offset of the bottom edge of the trapezoid. The default is 0.0.
1764
- offsetB : float , optional
1765
- The offset of the top edge of the trapezoid. The default is 0.0.
1766
- length : float , optional
1767
- The length of the trapezoid. The default is 1.0.
1768
- direction : list , optional
1769
- The vector representing the up direction of the trapezoid. The default is [0, 0, 1].
1770
- placement : str , optional
1771
- The description of the placement of the origin of the trapezoid. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1772
- tolerance : float , optional
1773
- The desired tolerance. The default is 0.0001.
1774
-
1775
- Returns
1776
- -------
1777
- topologic.Face
1778
- The created trapezoid.
1779
-
1780
- """
1781
- from topologicpy.Wire import Wire
1782
- wire = Wire.Trapezoid(origin=origin, widthA=widthA, widthB=widthB, offsetA=offsetA, offsetB=offsetB, length=length, direction=direction, placement=placement, tolerance=tolerance)
1783
- if not isinstance(wire, topologic.Wire):
1784
- print("Face.Rectangle - Error: Could not create the base wire for the trapezoid. Returning None.")
1785
- return None
1786
- return Face.ByWire(wire, tolerance=tolerance)
1787
-
1788
- @staticmethod
1789
- def Triangulate(face:topologic.Face, mode: int = 0, meshSize: float = None, tolerance: float = 0.0001) -> list:
1790
- """
1791
- Triangulates the input face and returns a list of faces.
1792
-
1793
- Parameters
1794
- ----------
1795
- face : topologic.Face
1796
- The input face.
1797
- tolerance : float , optional
1798
- The desired tolerance. The default is 0.0001.
1799
- mode : int , optional
1800
- The desired mode of meshing algorithm. Several options are available:
1801
- 0: Classic
1802
- 1: MeshAdapt
1803
- 3: Initial Mesh Only
1804
- 5: Delaunay
1805
- 6: Frontal-Delaunay
1806
- 7: BAMG
1807
- 8: Fontal-Delaunay for Quads
1808
- 9: Packing of Parallelograms
1809
- All options other than 0 (Classic) use the gmsh library. See https://gmsh.info/doc/texinfo/gmsh.html#Mesh-options
1810
- WARNING: The options that use gmsh can be very time consuming and can create very heavy geometry.
1811
- meshSize : float , optional
1812
- The desired size of the mesh when using the "mesh" option. If set to None, it will be
1813
- calculated automatically and set to 10% of the overall size of the face.
1814
-
1815
- Returns
1816
- -------
1817
- list
1818
- The list of triangles of the input face.
1819
-
1820
- """
1821
- from topologicpy.Wire import Wire
1822
- from topologicpy.Topology import Topology
1823
-
1824
- # This function was contributed by Yidan Xue.
1825
- def generate_gmsh(face, mode="mesh", meshSize = None, tolerance = 0.0001):
1826
- """
1827
- Creates a gmsh of triangular meshes from the input face.
1828
-
1829
- Parameters
1830
- ----------
1831
- face : topologic.Face
1832
- The input face.
1833
- meshSize : float , optional
1834
- The desired mesh size.
1835
- tolerance : float , optional
1836
- The desired tolerance. The default is 0.0001.
1837
-
1838
- Returns
1839
- ----------
1840
- topologic.Shell
1841
- The shell of triangular meshes.
1842
-
1843
- """
1844
- import os
1845
- import warnings
1846
- try:
1847
- import numpy as np
1848
- except:
1849
- print("Face.Triangulate - Warning: Installing required numpy library.")
1850
- try:
1851
- os.system("pip install numpy")
1852
- except:
1853
- os.system("pip install numpy --user")
1854
- try:
1855
- import numpy as np
1856
- print("Face.Triangulate - Warning: numpy library installed correctly.")
1857
- except:
1858
- warnings.warn("Face.Triangulate - Error: Could not import numpy. Please try to install numpy manually. Returning None.")
1859
- return None
1860
- try:
1861
- import gmsh
1862
- except:
1863
- print("Face.Triangulate - Warning: Installing required gmsh library.")
1864
- try:
1865
- os.system("pip install gmsh")
1866
- except:
1867
- os.system("pip install gmsh --user")
1868
- try:
1869
- import gmsh
1870
- print("Face.Triangulate - Warning: gmsh library installed correctly.")
1871
- except:
1872
- warnings.warn("Face.Triangulate - Error: Could not import gmsh. Please try to install gmsh manually. Returning None.")
1873
- return None
1874
-
1875
- import topologic
1876
- from topologicpy.Vertex import Vertex
1877
- from topologicpy.Wire import Wire
1878
- from topologicpy.Face import Face
1879
-
1880
- if not isinstance(face, topologic.Face):
1881
- print("Shell.ByMeshFace - Error: The input face parameter is not a valid face. Returning None.")
1882
- return None
1883
- if not meshSize:
1884
- bounding_face = Face.BoundingRectangle(face)
1885
- bounding_face_vertices = Face.Vertices(bounding_face)
1886
- bounding_face_vertices_x = [Vertex.X(i) for i in bounding_face_vertices]
1887
- bounding_face_vertices_y = [Vertex.Y(i) for i in bounding_face_vertices]
1888
- width = max(bounding_face_vertices_x)-min(bounding_face_vertices_x)
1889
- length = max(bounding_face_vertices_y)-min(bounding_face_vertices_y)
1890
- meshSize = max([width,length])//10
1891
-
1892
- gmsh.initialize()
1893
- face_external_boundary = Face.ExternalBoundary(face)
1894
- external_vertices = Wire.Vertices(face_external_boundary)
1895
- external_vertex_number = len(external_vertices)
1896
- for i in range(external_vertex_number):
1897
- gmsh.model.geo.addPoint(Vertex.X(external_vertices[i]), Vertex.Y(external_vertices[i]), Vertex.Z(external_vertices[i]), meshSize, i+1)
1898
- for i in range(external_vertex_number):
1899
- if i < external_vertex_number-1:
1900
- gmsh.model.geo.addLine(i+1, i+2, i+1)
1901
- else:
1902
- gmsh.model.geo.addLine(i+1, 1, i+1)
1903
- gmsh.model.geo.addCurveLoop([i+1 for i in range(external_vertex_number)], 1)
1904
- current_vertex_number = external_vertex_number
1905
- current_edge_number = external_vertex_number
1906
- current_wire_number = 1
1907
-
1908
- face_internal_boundaries = Face.InternalBoundaries(face)
1909
- if face_internal_boundaries:
1910
- internal_face_number = len(face_internal_boundaries)
1911
- for i in range(internal_face_number):
1912
- face_internal_boundary = face_internal_boundaries[i]
1913
- internal_vertices = Wire.Vertices(face_internal_boundary)
1914
- internal_vertex_number = len(internal_vertices)
1915
- for j in range(internal_vertex_number):
1916
- gmsh.model.geo.addPoint(Vertex.X(internal_vertices[j]), Vertex.Y(internal_vertices[j]), Vertex.Z(internal_vertices[j]), meshSize, current_vertex_number+j+1)
1917
- for j in range(internal_vertex_number):
1918
- if j < internal_vertex_number-1:
1919
- gmsh.model.geo.addLine(current_vertex_number+j+1, current_vertex_number+j+2, current_edge_number+j+1)
1920
- else:
1921
- gmsh.model.geo.addLine(current_vertex_number+j+1, current_vertex_number+1, current_edge_number+j+1)
1922
- gmsh.model.geo.addCurveLoop([current_edge_number+i+1 for i in range(internal_vertex_number)], current_wire_number+1)
1923
- current_vertex_number = current_vertex_number+internal_vertex_number
1924
- current_edge_number = current_edge_number+internal_vertex_number
1925
- current_wire_number = current_wire_number+1
1926
-
1927
- gmsh.model.geo.addPlaneSurface([i+1 for i in range(current_wire_number)])
1928
- gmsh.model.geo.synchronize()
1929
- if mode not in [1,3,5,6,7,8,9]:
1930
- mode = 6
1931
- gmsh.option.setNumber("Mesh.Algorithm", mode)
1932
- gmsh.model.mesh.generate(2) # For a 2D mesh
1933
- nodeTags, nodeCoords, nodeParams = gmsh.model.mesh.getNodes(-1, -1)
1934
- elemTypes, elemTags, elemNodeTags = gmsh.model.mesh.getElements(-1, -1)
1935
- gmsh.finalize()
1936
-
1937
- vertex_number = len(nodeTags)
1938
- vertices = []
1939
- for i in range(vertex_number):
1940
- vertices.append(Vertex.ByCoordinates(nodeCoords[3*i],nodeCoords[3*i+1],nodeCoords[3*i+2]))
1941
-
1942
- faces = []
1943
- for n in range(len(elemTypes)):
1944
- vn = elemTypes[n]+1
1945
- et = elemTags[n]
1946
- ent = elemNodeTags[n]
1947
- if vn==3:
1948
- for i in range(len(et)):
1949
- face_vertices = []
1950
- for j in range(vn):
1951
- face_vertices.append(vertices[np.where(nodeTags==ent[i*vn+j])[0][0]])
1952
- faces.append(Face.ByVertices(face_vertices))
1953
- return faces
1954
-
1955
- if not isinstance(face, topologic.Face):
1956
- print("Face.Triangulate - Error: The input face parameter is not a valid face. Returning None.")
1957
- return None
1958
- vertices = Topology.Vertices(face)
1959
- if len(vertices) == 3: # Already a triangle
1960
- return [face]
1961
- origin = Topology.Centroid(face)
1962
- normal = Face.Normal(face)
1963
- flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1964
-
1965
- if mode == 0:
1966
- shell_faces = []
1967
- for i in range(0,5,1):
1968
- try:
1969
- _ = topologic.FaceUtility.Triangulate(flatFace, float(i)*0.1, shell_faces)
1970
- break
1971
- except:
1972
- continue
1973
- else:
1974
- shell_faces = generate_gmsh(flatFace, mode = mode, meshSize = meshSize, tolerance = tolerance)
1975
-
1976
- if len(shell_faces) < 1:
1977
- return []
1978
- finalFaces = []
1979
- for f in shell_faces:
1980
- f = Topology.Unflatten(f, origin=origin, direction=normal)
1981
- if Face.Angle(face, f) > 90:
1982
- wire = Face.ExternalBoundary(f)
1983
- wire = Wire.Invert(wire)
1984
- f = topologic.Face.ByExternalBoundary(wire)
1985
- finalFaces.append(f)
1986
- else:
1987
- finalFaces.append(f)
1988
- return finalFaces
1989
-
1990
- @staticmethod
1991
- def TrimByWire(face: topologic.Face, wire: topologic.Wire, reverse: bool = False) -> topologic.Face:
1992
- """
1993
- Trims the input face by the input wire.
1994
-
1995
- Parameters
1996
- ----------
1997
- face : topologic.Face
1998
- The input face.
1999
- wire : topologic.Wire
2000
- The input wire.
2001
- reverse : bool , optional
2002
- If set to True, the effect of the trim will be reversed. The default is False.
2003
-
2004
- Returns
2005
- -------
2006
- topologic.Face
2007
- The resulting trimmed face.
2008
-
2009
- """
2010
- if not isinstance(face, topologic.Face):
2011
- return None
2012
- if not isinstance(wire, topologic.Wire):
2013
- return face
2014
- trimmed_face = topologic.FaceUtility.TrimByWire(face, wire, False)
2015
- if reverse:
2016
- trimmed_face = face.Difference(trimmed_face)
2017
- return trimmed_face
2018
-
2019
- @staticmethod
2020
- def VertexByParameters(face: topologic.Face, u: float = 0.5, v: float = 0.5) -> topologic.Vertex:
2021
- """
2022
- Creates a vertex at the *u* and *v* parameters of the input face.
2023
-
2024
- Parameters
2025
- ----------
2026
- face : topologic.Face
2027
- The input face.
2028
- u : float , optional
2029
- The *u* parameter of the input face. The default is 0.5.
2030
- v : float , optional
2031
- The *v* parameter of the input face. The default is 0.5.
2032
-
2033
- Returns
2034
- -------
2035
- vertex : topologic vertex
2036
- The created vertex.
2037
-
2038
- """
2039
- if not isinstance(face, topologic.Face):
2040
- return None
2041
- return topologic.FaceUtility.VertexAtParameters(face, u, v)
2042
-
2043
- @staticmethod
2044
- def VertexParameters(face: topologic.Face, vertex: topologic.Vertex, outputType: str = "uv", mantissa: int = 6) -> list:
2045
- """
2046
- Returns the *u* and *v* parameters of the input face at the location of the input vertex.
2047
-
2048
- Parameters
2049
- ----------
2050
- face : topologic.Face
2051
- The input face.
2052
- vertex : topologic.Vertex
2053
- The input vertex.
2054
- outputType : string , optional
2055
- The string defining the desired output. This can be any subset or permutation of "uv". It is case insensitive. The default is "uv".
2056
- mantissa : int , optional
2057
- The desired length of the mantissa. The default is 6.
2058
-
2059
- Returns
2060
- -------
2061
- list
2062
- The list of *u* and/or *v* as specified by the outputType input.
2063
-
2064
- """
2065
- if not isinstance(face, topologic.Face):
2066
- return None
2067
- if not isinstance(vertex, topologic.Vertex):
2068
- return None
2069
- params = topologic.FaceUtility.ParametersAtVertex(face, vertex)
2070
- u = round(params[0], mantissa)
2071
- v = round(params[1], mantissa)
2072
- outputType = list(outputType.lower())
2073
- returnResult = []
2074
- for param in outputType:
2075
- if param == "u":
2076
- returnResult.append(u)
2077
- elif param == "v":
2078
- returnResult.append(v)
2079
- return returnResult
2080
-
2081
- @staticmethod
2082
- def Vertices(face: topologic.Face) -> list:
2083
- """
2084
- Returns the vertices of the input face.
2085
-
2086
- Parameters
2087
- ----------
2088
- face : topologic.Face
2089
- The input face.
2090
-
2091
- Returns
2092
- -------
2093
- list
2094
- The list of vertices.
2095
-
2096
- """
2097
- if not isinstance(face, topologic.Face):
2098
- return None
2099
- vertices = []
2100
- _ = face.Vertices(None, vertices)
2101
- return vertices
2102
-
2103
- @staticmethod
2104
- def Wire(face: topologic.Face) -> topologic.Wire:
2105
- """
2106
- Returns the external boundary (closed wire) of the input face.
2107
-
2108
- Parameters
2109
- ----------
2110
- face : topologic.Face
2111
- The input face.
2112
-
2113
- Returns
2114
- -------
2115
- topologic.Wire
2116
- The external boundary of the input face.
2117
-
2118
- """
2119
- return face.ExternalBoundary()
2120
-
2121
- @staticmethod
2122
- def Wires(face: topologic.Face) -> list:
2123
- """
2124
- Returns the wires of the input face.
2125
-
2126
- Parameters
2127
- ----------
2128
- face : topologic.Face
2129
- The input face.
2130
-
2131
- Returns
2132
- -------
2133
- list
2134
- The list of wires.
2135
-
2136
- """
2137
- if not isinstance(face, topologic.Face):
2138
- return None
2139
- wires = []
2140
- _ = face.Wires(None, wires)
2141
- return wires
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 topologic_core as topologic
18
+ from topologicpy.Vector import Vector
19
+ from topologicpy.Wire import Wire
20
+ from topologicpy.Topology import Topology
21
+ import math
22
+ import os
23
+ import warnings
24
+
25
+ try:
26
+ import numpy as np
27
+ except:
28
+ print("Face - Installing required numpy library.")
29
+ try:
30
+ os.system("pip install numpy")
31
+ except:
32
+ os.system("pip install numpy --user")
33
+ try:
34
+ import numpy as np
35
+ print("Face - numpy library installed correctly.")
36
+ except:
37
+ warnings.warn("Face - Error: Could not import numpy.")
38
+
39
+ class Face(Topology):
40
+ @staticmethod
41
+ def AddInternalBoundaries(face: topologic.Face, wires: list) -> topologic.Face:
42
+ """
43
+ Adds internal boundaries (closed wires) to the input face. Internal boundaries are considered holes in the input face.
44
+
45
+ Parameters
46
+ ----------
47
+ face : topologic.Face
48
+ The input face.
49
+ wires : list
50
+ The input list of internal boundaries (closed wires).
51
+
52
+ Returns
53
+ -------
54
+ topologic.Face
55
+ The created face with internal boundaries added to it.
56
+
57
+ """
58
+
59
+ if not isinstance(face, topologic.Face):
60
+ print("Face.AddInternalBoundaries - Error: The input face parameter is not a valid topologic face. Returning None.")
61
+ return None
62
+ if not isinstance(wires, list):
63
+ print("Face.AddInternalBoundaries - Warning: The input wires parameter is not a valid list. Returning the input face.")
64
+ return face
65
+ wireList = [w for w in wires if isinstance(w, topologic.Wire)]
66
+ if len(wireList) < 1:
67
+ print("Face.AddInternalBoundaries - Warning: The input wires parameter does not contain any valid wires. Returning the input face.")
68
+ return face
69
+ faceeb = face.ExternalBoundary()
70
+ faceibList = []
71
+ _ = face.InternalBoundaries(faceibList)
72
+ for wire in wires:
73
+ faceibList.append(wire)
74
+ return topologic.Face.ByExternalInternalBoundaries(faceeb, faceibList)
75
+
76
+ @staticmethod
77
+ def AddInternalBoundariesCluster(face: topologic.Face, cluster: topologic.Cluster) -> topologic.Face:
78
+ """
79
+ Adds the input cluster of internal boundaries (closed wires) to the input face. Internal boundaries are considered holes in the input face.
80
+
81
+ Parameters
82
+ ----------
83
+ face : topologic.Face
84
+ The input face.
85
+ cluster : topologic.Cluster
86
+ The input cluster of internal boundaries (topologic wires).
87
+
88
+ Returns
89
+ -------
90
+ topologic.Face
91
+ The created face with internal boundaries added to it.
92
+
93
+ """
94
+ if not isinstance(face, topologic.Face):
95
+ print("Face.AddInternalBoundariesCluster - Warning: The input cluster parameter is not a valid cluster. Returning None.")
96
+ return None
97
+ if not cluster:
98
+ return face
99
+ if not isinstance(cluster, topologic.Cluster):
100
+ return face
101
+ wires = []
102
+ _ = cluster.Wires(None, wires)
103
+ return Face.AddInternalBoundaries(face, wires)
104
+
105
+ @staticmethod
106
+ def Angle(faceA: topologic.Face, faceB: topologic.Face, mantissa: int = 6) -> float:
107
+ """
108
+ Returns the angle in degrees between the two input faces.
109
+
110
+ Parameters
111
+ ----------
112
+ faceA : topologic.Face
113
+ The first input face.
114
+ faceB : topologic.Face
115
+ The second input face.
116
+ mantissa : int , optional
117
+ The desired length of the mantissa. The default is 6.
118
+
119
+ Returns
120
+ -------
121
+ float
122
+ The angle in degrees between the two input faces.
123
+
124
+ """
125
+ from topologicpy.Vector import Vector
126
+ if not isinstance(faceA, topologic.Face):
127
+ print("Face.Angle - Warning: The input faceA parameter is not a valid topologic face. Returning None.")
128
+ return None
129
+ if not isinstance(faceB, topologic.Face):
130
+ print("Face.Angle - Warning: The input faceB parameter is not a valid topologic face. Returning None.")
131
+ return None
132
+ dirA = Face.NormalAtParameters(faceA, 0.5, 0.5, "xyz", 3)
133
+ dirB = Face.NormalAtParameters(faceB, 0.5, 0.5, "xyz", 3)
134
+ return round((Vector.Angle(dirA, dirB)), mantissa)
135
+
136
+ @staticmethod
137
+ def Area(face: topologic.Face, mantissa: int = 6) -> float:
138
+ """
139
+ Returns the area of the input face.
140
+
141
+ Parameters
142
+ ----------
143
+ face : topologic.Face
144
+ The input face.
145
+ mantissa : int , optional
146
+ The desired length of the mantissa. The default is 6.
147
+
148
+ Returns
149
+ -------
150
+ float
151
+ The area of the input face.
152
+
153
+ """
154
+ if not isinstance(face, topologic.Face):
155
+ print("Face.Area - Warning: The input face parameter is not a valid topologic face. Returning None.")
156
+ return None
157
+ area = None
158
+ try:
159
+ area = round(topologic.FaceUtility.Area(face), mantissa)
160
+ except:
161
+ area = None
162
+ return area
163
+
164
+ @staticmethod
165
+ def BoundingRectangle(topology: topologic.Topology, optimize: int = 0, tolerance: float = 0.0001) -> topologic.Face:
166
+ """
167
+ Returns a face representing a bounding rectangle of the input topology. The returned face contains a dictionary with key "zrot" that represents rotations around the Z axis. If applied the resulting face will become axis-aligned.
168
+
169
+ Parameters
170
+ ----------
171
+ topology : topologic.Topology
172
+ The input topology.
173
+ optimize : int , optional
174
+ If set to an integer from 1 (low optimization) to 10 (high optimization), the method will attempt to optimize the bounding rectangle so that it reduces its surface area. The default is 0 which will result in an axis-aligned bounding rectangle. The default is 0.
175
+ tolerance : float , optional
176
+ The desired tolerance. The default is 0.0001.
177
+
178
+ Returns
179
+ -------
180
+ topologic.Face
181
+ The bounding rectangle of the input topology.
182
+
183
+ """
184
+ from topologicpy.Wire import Wire
185
+ from topologicpy.Face import Face
186
+ from topologicpy.Cluster import Cluster
187
+ from topologicpy.Topology import Topology
188
+ from topologicpy.Dictionary import Dictionary
189
+ def bb(topology):
190
+ vertices = []
191
+ _ = topology.Vertices(None, vertices)
192
+ x = []
193
+ y = []
194
+ for aVertex in vertices:
195
+ x.append(aVertex.X())
196
+ y.append(aVertex.Y())
197
+ minX = min(x)
198
+ minY = min(y)
199
+ maxX = max(x)
200
+ maxY = max(y)
201
+ return [minX, minY, maxX, maxY]
202
+
203
+ if not isinstance(topology, topologic.Topology):
204
+ print("Face.BoundingRectangle - Warning: The input topology parameter is not a valid topologic topology. Returning None.")
205
+ return None
206
+ vertices = Topology.SubTopologies(topology, subTopologyType="vertex")
207
+ topology = Cluster.ByTopologies(vertices)
208
+ boundingBox = bb(topology)
209
+ minX = boundingBox[0]
210
+ minY = boundingBox[1]
211
+ maxX = boundingBox[2]
212
+ maxY = boundingBox[3]
213
+ w = abs(maxX - minX)
214
+ l = abs(maxY - minY)
215
+ best_area = l*w
216
+ orig_area = best_area
217
+ best_z = 0
218
+ best_bb = boundingBox
219
+ origin = Topology.Centroid(topology)
220
+ optimize = min(max(optimize, 0), 10)
221
+ if optimize > 0:
222
+ factor = (round(((11 - optimize)/30 + 0.57), 2))
223
+ flag = False
224
+ for n in range(10,0,-1):
225
+ if flag:
226
+ break
227
+ za = n
228
+ zb = 90+n
229
+ zc = n
230
+ for z in range(za,zb,zc):
231
+ if flag:
232
+ break
233
+ t = Topology.Rotate(topology, origin=origin, axis=[0, 0, 1], angle=z)
234
+ minX, minY, maxX, maxY = bb(t)
235
+ w = abs(maxX - minX)
236
+ l = abs(maxY - minY)
237
+ area = l*w
238
+ if area < orig_area*factor:
239
+ best_area = area
240
+ best_z = z
241
+ best_bb = [minX, minY, maxX, maxY]
242
+ flag = True
243
+ break
244
+ if area < best_area:
245
+ best_area = area
246
+ best_z = z
247
+ best_bb = [minX, minY, maxX, maxY]
248
+
249
+ else:
250
+ best_bb = boundingBox
251
+
252
+ minX, minY, maxX, maxY = best_bb
253
+ vb1 = topologic.Vertex.ByCoordinates(minX, minY, 0)
254
+ vb2 = topologic.Vertex.ByCoordinates(maxX, minY, 0)
255
+ vb3 = topologic.Vertex.ByCoordinates(maxX, maxY, 0)
256
+ vb4 = topologic.Vertex.ByCoordinates(minX, maxY, 0)
257
+
258
+ baseWire = Wire.ByVertices([vb1, vb2, vb3, vb4], close=True)
259
+ baseFace = Face.ByWire(baseWire, tolerance=tolerance)
260
+ baseFace = Topology.Rotate(baseFace, origin=origin, axis=[0, 0, 1], angle=-best_z)
261
+ dictionary = Dictionary.ByKeysValues(["zrot"], [best_z])
262
+ baseFace = Topology.SetDictionary(baseFace, dictionary)
263
+ return baseFace
264
+
265
+ @staticmethod
266
+ def ByEdges(edges: list, tolerance : float = 0.0001) -> topologic.Face:
267
+ """
268
+ Creates a face from the input list of edges.
269
+
270
+ Parameters
271
+ ----------
272
+ edges : list
273
+ The input list of edges.
274
+ tolerance : float , optional
275
+ The desired tolerance. The default is 0.0001.
276
+
277
+ Returns
278
+ -------
279
+ face : topologic.Face
280
+ The created face.
281
+
282
+ """
283
+ from topologicpy.Wire import Wire
284
+ if not isinstance(edges, list):
285
+ print("Face.ByEdges - Error: The input edges parameter is not a valid list. Returning None.")
286
+ return None
287
+ edges = [e for e in edges if isinstance(e, topologic.Edge)]
288
+ if len(edges) < 1:
289
+ print("Face.ByEdges - Error: The input edges parameter does not contain any valid edges. Returning None.")
290
+ return None
291
+ wire = Wire.ByEdges(edges, tolerance=tolerance)
292
+ if not isinstance(wire, topologic.Wire):
293
+ print("Face.ByEdges - Error: Could not create the required wire. Returning None.")
294
+ return None
295
+ return Face.ByWire(wire, tolerance=tolerance)
296
+
297
+ @staticmethod
298
+ def ByEdgesCluster(cluster: topologic.Cluster, tolerance: float = 0.0001) -> topologic.Face:
299
+ """
300
+ Creates a face from the input cluster of edges.
301
+
302
+ Parameters
303
+ ----------
304
+ cluster : topologic.Cluster
305
+ The input cluster of edges.
306
+ tolerance : float , optional
307
+ The desired tolerance. The default is 0.0001.
308
+
309
+ Returns
310
+ -------
311
+ face : topologic.Face
312
+ The created face.
313
+
314
+ """
315
+ from topologicpy.Cluster import Cluster
316
+ if not isinstance(cluster, topologic.Cluster):
317
+ print("Face.ByEdgesCluster - Warning: The input cluster parameter is not a valid topologic cluster. Returning None.")
318
+ return None
319
+ edges = Cluster.Edges(cluster)
320
+ if len(edges) < 1:
321
+ print("Face.ByEdgesCluster - Warning: The input cluster parameter does not contain any valid edges. Returning None.")
322
+ return None
323
+ return Face.ByEdges(edges, tolerance=tolerance)
324
+
325
+ @staticmethod
326
+ def ByOffset(face: topologic.Face, offset: float = 1.0, miter: bool = False,
327
+ miterThreshold: float = None, offsetKey: str = None,
328
+ miterThresholdKey: str = None, step: bool = True, tolerance: float = 0.0001) -> topologic.Face:
329
+ """
330
+ Creates an offset face from the input face.
331
+
332
+ Parameters
333
+ ----------
334
+ face : topologic.Face
335
+ The input face.
336
+ offset : float , optional
337
+ The desired offset distance. The default is 1.0.
338
+ miter : bool , optional
339
+ if set to True, the corners will be mitered. The default is False.
340
+ miterThreshold : float , optional
341
+ The distance beyond which a miter should be added. The default is None which means the miter threshold is set to the offset distance multiplied by the square root of 2.
342
+ offsetKey : str , optional
343
+ If specified, the dictionary of the edges will be queried for this key to sepcify the desired offset. The default is None.
344
+ miterThresholdKey : str , optional
345
+ If specified, the dictionary of the vertices will be queried for this key to sepcify the desired miter threshold distance. The default is None.
346
+ step : bool , optional
347
+ If set to True, The transition between collinear edges with different offsets will be a step. Otherwise, it will be a continous edge. The default is True.
348
+ tolerance : float , optional
349
+ The desired tolerance. The default is 0.0001.
350
+
351
+ Returns
352
+ -------
353
+ topologic.Face
354
+ The created face.
355
+
356
+ """
357
+ from topologicpy.Wire import Wire
358
+ if not isinstance(face, topologic.Face):
359
+ print("Face.ByOffset - Warning: The input face parameter is not a valid toplogic face. Returning None.")
360
+ return None
361
+ eb = Face.Wire(face)
362
+ internal_boundaries = Face.InternalBoundaries(face)
363
+ offset_external_boundary = Wire.ByOffset(wire=eb, offset=offset, miter=miter, miterThreshold=miterThreshold, offsetKey=offsetKey, miterThresholdKey=miterThresholdKey, step=step)
364
+ offset_internal_boundaries = []
365
+ for internal_boundary in internal_boundaries:
366
+ offset_internal_boundaries.append(Wire.ByOffset(wire=internal_boundary, offset=offset, miter=miter, miterThreshold=miterThreshold, offsetKey=offsetKey, miterThresholdKey=miterThresholdKey, step=step))
367
+ return Face.ByWires(offset_external_boundary, offset_internal_boundaries, tolerance=tolerance)
368
+
369
+ @staticmethod
370
+ def ByShell(shell: topologic.Shell, origin: topologic.Vertex = None, angTolerance: float = 0.1, tolerance: float = 0.0001)-> topologic.Face:
371
+ """
372
+ Creates a face by merging the faces of the input shell.
373
+
374
+ Parameters
375
+ ----------
376
+ shell : topologic.Shell
377
+ The input shell.
378
+ angTolerance : float , optional
379
+ The desired angular tolerance. The default is 0.1.
380
+ tolerance : float , optional
381
+ The desired tolerance. The default is 0.0001.
382
+
383
+ Returns
384
+ -------
385
+ topologic.Face
386
+ The created face.
387
+
388
+ """
389
+ from topologicpy.Vertex import Vertex
390
+ from topologicpy.Wire import Wire
391
+ from topologicpy.Shell import Shell
392
+ from topologicpy.Cluster import Cluster
393
+ from topologicpy.Topology import Topology
394
+ from topologicpy.Dictionary import Dictionary
395
+ from topologicpy.Helper import Helper
396
+
397
+ def planarizeList(wireList):
398
+ returnList = []
399
+ for aWire in wireList:
400
+ returnList.append(Wire.Planarize(aWire))
401
+ return returnList
402
+
403
+ if not isinstance(shell, topologic.Shell):
404
+ print("Face.ByShell - Error: The input shell parameter is not a valid toplogic shell. Returning None.")
405
+ return None
406
+
407
+ if origin == None:
408
+ origin = Topology.Centroid(shell)
409
+ if not isinstance(origin, topologic.Vertex):
410
+ print("Face.ByShell - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
411
+ return None
412
+
413
+ # Try the simple method first
414
+ face = None
415
+ ext_boundary = Wire.RemoveCollinearEdges(Shell.ExternalBoundary(shell))
416
+ if isinstance(ext_boundary, topologic.Wire):
417
+ face = Face.ByWire(ext_boundary)
418
+ elif isinstance(ext_boundary, topologic.Cluster):
419
+ wires = Topology.Wires(ext_boundary)
420
+ faces = [Face.ByWire(w) for w in wires]
421
+ areas = [Face.Area(f) for f in faces]
422
+ wires = Helper.Sort(wires, areas, reverseFlags=[True])
423
+ face = Face.ByWires(wires[0], wires[1:])
424
+
425
+ if isinstance(face, topologic.Face):
426
+ return face
427
+ world_origin = Vertex.Origin()
428
+ planar_shell = Shell.Planarize(shell)
429
+ normal = Face.Normal(Topology.Faces(planar_shell)[0])
430
+ planar_shell = Topology.Flatten(planar_shell, origin=origin, direction=normal)
431
+ vertices = Shell.Vertices(planar_shell)
432
+ new_vertices = []
433
+ for v in vertices:
434
+ x, y, z = Vertex.Coordinates(v)
435
+ new_v = Vertex.ByCoordinates(x,y,0)
436
+ new_vertices.append(new_v)
437
+ planar_shell = Topology.SelfMerge(Topology.ReplaceVertices(planar_shell, verticesA=vertices, verticesB=new_vertices), tolerance=tolerance)
438
+ ext_boundary = Shell.ExternalBoundary(planar_shell, tolerance=tolerance)
439
+ ext_boundary = Topology.RemoveCollinearEdges(ext_boundary, angTolerance)
440
+ if not isinstance(ext_boundary, topologic.Topology):
441
+ print("Face.ByShell - Error: Could not derive the external boundary of the input shell parameter. Returning None.")
442
+ return None
443
+
444
+ if isinstance(ext_boundary, topologic.Wire):
445
+ if not Topology.IsPlanar(ext_boundary, tolerance=tolerance):
446
+ ext_boundary = Wire.Planarize(ext_boundary, origin=origin, tolerance=tolerance)
447
+ ext_boundary = Topology.RemoveCollinearEdges(ext_boundary, angTolerance)
448
+ try:
449
+ face = Face.ByWire(ext_boundary)
450
+ face = Topology.Unflatten(face, origin=origin, direction=normal)
451
+ return face
452
+ except:
453
+ print("Face.ByShell - Error: The operation failed. Returning None.")
454
+ return None
455
+ elif isinstance(ext_boundary, topologic.Cluster): # The shell has holes.
456
+ wires = []
457
+ _ = ext_boundary.Wires(None, wires)
458
+ faces = []
459
+ areas = []
460
+ for wire in wires:
461
+ aFace = Face.ByWire(wire, tolerance=tolerance)
462
+ if not isinstance(aFace, topologic.Face):
463
+ print("Face.ByShell - Error: The operation failed. Returning None.")
464
+ return None
465
+ anArea = abs(Face.Area(aFace))
466
+ faces.append(aFace)
467
+ areas.append(anArea)
468
+ max_index = areas.index(max(areas))
469
+ ext_boundary = faces[max_index]
470
+ int_boundaries = list(set(faces) - set([ext_boundary]))
471
+ int_wires = []
472
+ for int_boundary in int_boundaries:
473
+ temp_wires = []
474
+ _ = int_boundary.Wires(None, temp_wires)
475
+ int_wires.append(Topology.RemoveCollinearEdges(temp_wires[0], angTolerance))
476
+ temp_wires = []
477
+ _ = ext_boundary.Wires(None, temp_wires)
478
+ ext_wire = Topology.RemoveCollinearEdges(temp_wires[0], angTolerance)
479
+ face = Face.ByWires(ext_wire, int_wires)
480
+
481
+ face = Topology.Unflatten(face, origin=origin, direction=normal)
482
+ return face
483
+ else:
484
+ return None
485
+
486
+ @staticmethod
487
+ def ByVertices(vertices: list, tolerance: float = 0.0001) -> topologic.Face:
488
+
489
+ """
490
+ Creates a face from the input list of vertices.
491
+
492
+ Parameters
493
+ ----------
494
+ vertices : list
495
+ The input list of vertices.
496
+ tolerance : float , optional
497
+ The desired tolerance. The default is 0.0001.
498
+
499
+ Returns
500
+ -------
501
+ topologic.Face
502
+ The created face.
503
+
504
+ """
505
+ from topologicpy.Topology import Topology
506
+ from topologicpy.Wire import Wire
507
+
508
+ if not isinstance(vertices, list):
509
+ return None
510
+ vertexList = [x for x in vertices if isinstance(x, topologic.Vertex)]
511
+ if len(vertexList) < 3:
512
+ return None
513
+
514
+ w = Wire.ByVertices(vertexList, tolerance=tolerance)
515
+ f = Face.ByWire(w, tolerance=tolerance)
516
+ return f
517
+
518
+ @staticmethod
519
+ def ByVerticesCluster(cluster: topologic.Cluster, tolerance: float = 0.0001) -> topologic.Face:
520
+ """
521
+ Creates a face from the input cluster of vertices.
522
+
523
+ Parameters
524
+ ----------
525
+ cluster : topologic.Cluster
526
+ The input cluster of vertices.
527
+ tolerance : float , optional
528
+ The desired tolerance. The default is 0.0001.
529
+
530
+ Returns
531
+ -------
532
+ topologic.Face
533
+ The crearted face.
534
+
535
+ """
536
+ from topologicpy.Cluster import Cluster
537
+ if not isinstance(cluster, topologic.Cluster):
538
+ return None
539
+ vertices = Cluster.Vertices(cluster)
540
+ return Face.ByVertices(vertices, tolerance=tolerance)
541
+
542
+ @staticmethod
543
+ def ByWire(wire: topologic.Wire, tolerance: float = 0.0001, silent=False) -> topologic.Face:
544
+ """
545
+ Creates a face from the input closed wire.
546
+
547
+ Parameters
548
+ ----------
549
+ wire : topologic.Wire
550
+ The input wire.
551
+ tolerance : float , optional
552
+ The desired tolerance. The default is 0.0001.
553
+ silent : bool , optional
554
+ If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
555
+
556
+ Returns
557
+ -------
558
+ topologic.Face or list
559
+ The created face. If the wire is non-planar, the method will attempt to triangulate the wire and return a list of faces.
560
+
561
+ """
562
+ from topologicpy.Vertex import Vertex
563
+ from topologicpy.Wire import Wire
564
+ from topologicpy.Shell import Shell
565
+ from topologicpy.Cluster import Cluster
566
+ from topologicpy.Topology import Topology
567
+ from topologicpy.Dictionary import Dictionary
568
+ import random
569
+
570
+ def triangulateWire(wire):
571
+ wire = Topology.RemoveCollinearEdges(wire)
572
+ vertices = Topology.Vertices(wire)
573
+ shell = Shell.Delaunay(vertices)
574
+ if isinstance(shell, topologic.Topology):
575
+ return Topology.Faces(shell)
576
+ else:
577
+ return []
578
+ if not isinstance(wire, topologic.Wire):
579
+ if not silent:
580
+ print("Face.ByWire - Error: The input wire parameter is not a valid topologic wire. Returning None.")
581
+ return None
582
+ if not Wire.IsClosed(wire):
583
+ if not silent:
584
+ print("Face.ByWire - Error: The input wire parameter is not a closed topologic wire. Returning None.")
585
+ return None
586
+
587
+ edges = Wire.Edges(wire)
588
+ wire = Topology.SelfMerge(Cluster.ByTopologies(edges), tolerance=tolerance)
589
+ vertices = Topology.Vertices(wire)
590
+ fList = []
591
+ if isinstance(wire, topologic.Wire):
592
+ try:
593
+ fList = topologic.Face.ByExternalBoundary(wire)
594
+ except:
595
+ if not silent:
596
+ print("Face.ByWire - Warning: Could not create face by external boundary. Trying other methods.")
597
+ if len(vertices) > 3:
598
+ fList = triangulateWire(wire)
599
+ else:
600
+ fList = []
601
+
602
+ if not isinstance(fList, list):
603
+ fList = [fList]
604
+
605
+ returnList = []
606
+ for f in fList:
607
+ if Face.Area(f) < 0:
608
+ wire = Face.ExternalBoundary(f)
609
+ wire = Wire.Invert(wire)
610
+ try:
611
+ f = topologic.Face.ByExternalBoundary(wire)
612
+ returnList.append(f)
613
+ except:
614
+ pass
615
+ else:
616
+ returnList.append(f)
617
+ if len(returnList) == 0:
618
+ if not silent:
619
+ print("Face.ByWire - Error: Could not build a face from the input wire parameter. Returning None.")
620
+ return None
621
+ elif len(returnList) == 1:
622
+ return returnList[0]
623
+ else:
624
+ if not silent:
625
+ print("Face.ByWire - Warning: Could not build a single face from the input wire parameter. Returning a list of faces.")
626
+ return returnList
627
+
628
+ @staticmethod
629
+ def ByWires(externalBoundary: topologic.Wire, internalBoundaries: list = [], tolerance: float = 0.0001, silent: bool = False) -> topologic.Face:
630
+ """
631
+ Creates a face from the input external boundary (closed wire) and the input list of internal boundaries (closed wires).
632
+
633
+ Parameters
634
+ ----------
635
+ externalBoundary : topologic.Wire
636
+ The input external boundary.
637
+ internalBoundaries : list , optional
638
+ The input list of internal boundaries (closed wires). The default is an empty list.
639
+ tolerance : float , optional
640
+ The desired tolerance. The default is 0.0001.
641
+ silent : bool , optional
642
+ If set to False, error messages are printed. Otherwise, they are not. The default is False.
643
+
644
+ Returns
645
+ -------
646
+ topologic.Face
647
+ The created face.
648
+
649
+ """
650
+ if not isinstance(externalBoundary, topologic.Wire):
651
+ if not silent:
652
+ print("Face.ByWires - Error: The input externalBoundary parameter is not a valid topologic wire. Returning None.")
653
+ return None
654
+ if not Wire.IsClosed(externalBoundary):
655
+ if not silent:
656
+ print("Face.ByWires - Error: The input externalBoundary parameter is not a closed topologic wire. Returning None.")
657
+ return None
658
+ ibList = [x for x in internalBoundaries if isinstance(x, topologic.Wire) and Wire.IsClosed(x)]
659
+ face = None
660
+ try:
661
+ face = topologic.Face.ByExternalInternalBoundaries(externalBoundary, ibList, tolerance)
662
+ except:
663
+ if not silent:
664
+ print("Face.ByWires - Error: The operation failed. Returning None.")
665
+ face = None
666
+ return face
667
+
668
+
669
+ @staticmethod
670
+ def ByWiresCluster(externalBoundary: topologic.Wire, internalBoundariesCluster: topologic.Cluster = None, tolerance: float = 0.0001, silent: bool = False) -> topologic.Face:
671
+ """
672
+ Creates a face from the input external boundary (closed wire) and the input cluster of internal boundaries (closed wires).
673
+
674
+ Parameters
675
+ ----------
676
+ externalBoundary : topologic.Wire
677
+ The input external boundary (closed wire).
678
+ internalBoundariesCluster : topologic.Cluster
679
+ The input cluster of internal boundaries (closed wires). The default is None.
680
+ tolerance : float , optional
681
+ The desired tolerance. The default is 0.0001.
682
+ silent : bool , optional
683
+ If set to False, error messages are printed. Otherwise, they are not. The default is False.
684
+
685
+ Returns
686
+ -------
687
+ topologic.Face
688
+ The created face.
689
+
690
+ """
691
+ from topologicpy.Wire import Wire
692
+ from topologicpy.Cluster import Cluster
693
+ if not isinstance(externalBoundary, topologic.Wire):
694
+ if not silent:
695
+ print("Face.ByWiresCluster - Error: The input externalBoundary parameter is not a valid topologic wire. Returning None.")
696
+ return None
697
+ if not Wire.IsClosed(externalBoundary):
698
+ if not silent:
699
+ print("Face.ByWiresCluster - Error: The input externalBoundary parameter is not a closed topologic wire. Returning None.")
700
+ return None
701
+ if not internalBoundariesCluster:
702
+ internalBoundaries = []
703
+ elif not isinstance(internalBoundariesCluster, topologic.Cluster):
704
+ if not silent:
705
+ print("Face.ByWiresCluster - Error: The input internalBoundariesCluster parameter is not a valid topologic cluster. Returning None.")
706
+ return None
707
+ else:
708
+ internalBoundaries = Cluster.Wires(internalBoundariesCluster)
709
+ return Face.ByWires(externalBoundary, internalBoundaries, tolerance=tolerance, silent=silent)
710
+
711
+ @staticmethod
712
+ def NorthArrow(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 16, direction: list = [0, 0, 1], northAngle: float = 0.0,
713
+ placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
714
+ """
715
+ Creates a north arrow.
716
+
717
+ Parameters
718
+ ----------
719
+ origin : topologic.Vertex, optional
720
+ The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
721
+ radius : float , optional
722
+ The radius of the circle. The default is 1.
723
+ sides : int , optional
724
+ The number of sides of the circle. The default is 16.
725
+ direction : list , optional
726
+ The vector representing the up direction of the circle. The default is [0, 0, 1].
727
+ northAngle : float , optional
728
+ The angular offset in degrees from the positive Y axis direction. The angle is measured in a counter-clockwise fashion where 0 is positive Y, 90 is negative X, 180 is negative Y, and 270 is positive X.
729
+ placement : str , optional
730
+ The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
731
+ tolerance : float , optional
732
+ The desired tolerance. The default is 0.0001.
733
+
734
+ Returns
735
+ -------
736
+ topologic.Face
737
+ The created circle.
738
+
739
+ """
740
+ from topologicpy.Topology import Topology
741
+ from topologicpy.Vertex import Vertex
742
+ if not origin:
743
+ origin = Vertex.Origin()
744
+
745
+ c = Face.Circle(origin=origin, radius=radius, sides=sides, direction=[0, 0, 1], placement="center", tolerance=tolerance)
746
+ r = Face.Rectangle(origin=origin, width=radius*0.01,length=radius*1.2, placement="lowerleft")
747
+ r = Topology.Translate(r, -0.005*radius,0,0)
748
+ arrow = Topology.Difference(c, r, tolerance=tolerance)
749
+ arrow = Topology.Rotate(arrow, origin=Vertex.Origin(), axis=[0, 0, 1], angle=northAngle)
750
+ if placement.lower() == "lowerleft":
751
+ arrow = Topology.Translate(arrow, radius, radius, 0)
752
+ elif placement.lower() == "upperleft":
753
+ arrow = Topology.Translate(arrow, radius, -radius, 0)
754
+ elif placement.lower() == "lowerright":
755
+ arrow = Topology.Translate(arrow, -radius, radius, 0)
756
+ elif placement.lower() == "upperright":
757
+ arrow = Topology.Translate(arrow, -radius, -radius, 0)
758
+ arrow = Topology.Place(arrow, originA=Vertex.Origin(), originB=origin)
759
+ arrow = Topology.Orient(arrow, orign=origin, direction=direction)
760
+ return arrow
761
+
762
+ @staticmethod
763
+ def Circle(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 16, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1],
764
+ placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
765
+ """
766
+ Creates a circle.
767
+
768
+ Parameters
769
+ ----------
770
+ origin : topologic.Vertex, optional
771
+ The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
772
+ radius : float , optional
773
+ The radius of the circle. The default is 1.
774
+ sides : int , optional
775
+ The number of sides of the circle. The default is 16.
776
+ fromAngle : float , optional
777
+ The angle in degrees from which to start creating the arc of the circle. The default is 0.
778
+ toAngle : float , optional
779
+ The angle in degrees at which to end creating the arc of the circle. The default is 360.
780
+ direction : list , optional
781
+ The vector representing the up direction of the circle. The default is [0, 0, 1].
782
+ placement : str , optional
783
+ The description of the placement of the origin of the circle. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
784
+ tolerance : float , optional
785
+ The desired tolerance. The default is 0.0001.
786
+
787
+ Returns
788
+ -------
789
+ topologic.Face
790
+ The created circle.
791
+
792
+ """
793
+ from topologicpy.Wire import Wire
794
+ wire = Wire.Circle(origin=origin, radius=radius, sides=sides, fromAngle=fromAngle, toAngle=toAngle, close=True, direction=direction, placement=placement, tolerance=tolerance)
795
+ if not isinstance(wire, topologic.Wire):
796
+ return None
797
+ return Face.ByWire(wire, tolerance=tolerance)
798
+
799
+ @staticmethod
800
+ def Compactness(face: topologic.Face, mantissa: int = 6) -> float:
801
+ """
802
+ Returns the compactness measure of the input face. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
803
+
804
+ Parameters
805
+ ----------
806
+ face : topologic.Face
807
+ The input face.
808
+ mantissa : int , optional
809
+ The desired length of the mantissa. The default is 6.
810
+
811
+ Returns
812
+ -------
813
+ float
814
+ The compactness measure of the input face.
815
+
816
+ """
817
+ exb = face.ExternalBoundary()
818
+ edges = []
819
+ _ = exb.Edges(None, edges)
820
+ perimeter = 0.0
821
+ for anEdge in edges:
822
+ perimeter = perimeter + abs(topologic.EdgeUtility.Length(anEdge))
823
+ area = abs(Face.Area(face))
824
+ compactness = 0
825
+ #From https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
826
+
827
+ if area <= 0:
828
+ return None
829
+ if perimeter <= 0:
830
+ return None
831
+ compactness = (math.pi*(2*math.sqrt(area/math.pi)))/perimeter
832
+ return round(compactness, mantissa)
833
+
834
+ @staticmethod
835
+ def CompassAngle(face: topologic.Face, north: list = None, mantissa: int = 6) -> float:
836
+ """
837
+ Returns the horizontal compass angle in degrees between the normal vector of the input face and the input vector. The angle is measured in counter-clockwise fashion. Only the first two elements of the vectors are considered.
838
+
839
+ Parameters
840
+ ----------
841
+ face : topologic.Face
842
+ The input face.
843
+ north : list , optional
844
+ The second vector representing the north direction. The default is the positive YAxis ([0,1,0]).
845
+ mantissa : int, optional
846
+ The length of the desired mantissa. The default is 6.
847
+ tolerance : float , optional
848
+ The desired tolerance. The default is 0.0001.
849
+
850
+ Returns
851
+ -------
852
+ float
853
+ The horizontal compass angle in degrees between the direction of the face and the second input vector.
854
+
855
+ """
856
+ from topologicpy.Vector import Vector
857
+ if not isinstance(face, topologic.Face):
858
+ return None
859
+ if not north:
860
+ north = Vector.North()
861
+ dirA = Face.NormalAtParameters(face,mantissa=mantissa)
862
+ return Vector.CompassAngle(vectorA=dirA, vectorB=north, mantissa=mantissa)
863
+
864
+ @staticmethod
865
+ def Edges(face: topologic.Face) -> list:
866
+ """
867
+ Returns the edges of the input face.
868
+
869
+ Parameters
870
+ ----------
871
+ face : topologic.Face
872
+ The input face.
873
+
874
+ Returns
875
+ -------
876
+ list
877
+ The list of edges.
878
+
879
+ """
880
+ if not isinstance(face, topologic.Face):
881
+ return None
882
+ edges = []
883
+ _ = face.Edges(None, edges)
884
+ return edges
885
+
886
+ @staticmethod
887
+ def Einstein(origin: topologic.Vertex = None, radius: float = 0.5, direction: list = [0, 0, 1],
888
+ placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
889
+ """
890
+ Creates an aperiodic monotile, also called an 'einstein' tile (meaning one tile in German, not the name of the famous physist). See https://arxiv.org/abs/2303.10798
891
+
892
+ Parameters
893
+ ----------
894
+ origin : topologic.Vertex , optional
895
+ The location of the origin of the tile. The default is None which results in the tiles first vertex being placed at (0, 0, 0).
896
+ radius : float , optional
897
+ The radius of the hexagon determining the size of the tile. The default is 0.5.
898
+ direction : list , optional
899
+ The vector representing the up direction of the ellipse. The default is [0, 0, 1].
900
+ placement : str , optional
901
+ The description of the placement of the origin of the hexagon determining the location of the tile. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
902
+ tolerance : float , optional
903
+ The desired tolerance. The default is 0.0001.
904
+
905
+ Returns
906
+ --------
907
+ topologic.Face
908
+ The created Einstein tile.
909
+
910
+ """
911
+ from topologicpy.Wire import Wire
912
+
913
+ wire = Wire.Einstein(origin=origin, radius=radius, direction=direction, placement=placement)
914
+ if not isinstance(wire, topologic.Wire):
915
+ print("Face.Einstein - Error: Could not create base wire for the Einstein tile. Returning None.")
916
+ return None
917
+ return Face.ByWire(wire, tolerance=tolerance)
918
+
919
+ @staticmethod
920
+ def ExteriorAngles(face: topologic.Face, includeInternalBoundaries=False, mantissa: int = 6) -> list:
921
+ """
922
+ Returns the exterior angles of the input face in degrees. The face must be planar.
923
+
924
+ Parameters
925
+ ----------
926
+ face : topologic.Face
927
+ The input face.
928
+ includeInternalBoundaries : bool , optional
929
+ If set to True and if the face has internal boundaries (holes), the returned list will be a nested list where the first list is the list
930
+ of exterior angles of the external boundary and the second list will contain lists of the exterior angles of each of the
931
+ internal boundaries (holes). For example: [[270,270,270,270], [[270,270,270,270],[300,300,300]]]. If not, the returned list will be
932
+ a simple list of interior angles of the external boundary. For example: [270,270,270,270]. Please note that that the interior angles of the
933
+ internal boundaries are considered to be those interior to the original face. Thus, they are exterior to the internal boundary.
934
+ mantissa : int , optional
935
+ The desired length of the mantissa. The default is 6.
936
+ Returns
937
+ -------
938
+ list
939
+ The list of exterior angles.
940
+ """
941
+
942
+ from topologicpy.Wire import Wire
943
+
944
+ if not isinstance(face, topologic.Face):
945
+ print("Face.ExteriorAngles - Error: The input face parameter is not a valid face. Returning None.")
946
+ return None
947
+ eb = Face.ExternalBoundary(face)
948
+ return_list = Wire.ExteriorAngles(eb, mantissa=mantissa)
949
+ if includeInternalBoundaries:
950
+ internal_boundaries = Face.InternalBoundaries(face)
951
+ ib_i_a_list = []
952
+ if len(internal_boundaries) > 0:
953
+ for ib in internal_boundaries:
954
+ ib_interior_angles = Wire.InteriorAngles(ib, mantissa=mantissa)
955
+ ib_i_a_list.append(ib_interior_angles)
956
+ if len(ib_i_a_list) > 0:
957
+ return_list = [return_list]+[ib_i_a_list]
958
+ return return_list
959
+
960
+ @staticmethod
961
+ def ExternalBoundary(face: topologic.Face) -> topologic.Wire:
962
+ """
963
+ Returns the external boundary (closed wire) of the input face.
964
+
965
+ Parameters
966
+ ----------
967
+ face : topologic.Face
968
+ The input face.
969
+
970
+ Returns
971
+ -------
972
+ topologic.Wire
973
+ The external boundary of the input face.
974
+
975
+ """
976
+ return face.ExternalBoundary()
977
+
978
+ @staticmethod
979
+ def FacingToward(face: topologic.Face, direction: list = [0,0,-1], asVertex: bool = False, tolerance: float = 0.0001) -> bool:
980
+ """
981
+ Returns True if the input face is facing toward the input direction.
982
+
983
+ Parameters
984
+ ----------
985
+ face : topologic.Face
986
+ The input face.
987
+ direction : list , optional
988
+ The input direction. The default is [0,0,-1].
989
+ asVertex : bool , optional
990
+ If set to True, the direction is treated as an actual vertex in 3D space. The default is False.
991
+ tolerance : float , optional
992
+ The desired tolerance. The default is 0.0001.
993
+
994
+ Returns
995
+ -------
996
+ bool
997
+ True if the face is facing toward the direction. False otherwise.
998
+
999
+ """
1000
+ faceNormal = topologic.FaceUtility.NormalAtParameters(face,0.5, 0.5)
1001
+ faceCenter = topologic.FaceUtility.VertexAtParameters(face,0.5,0.5)
1002
+ cList = [faceCenter.X(), faceCenter.Y(), faceCenter.Z()]
1003
+ try:
1004
+ vList = [direction.X(), direction.Y(), direction.Z()]
1005
+ except:
1006
+ try:
1007
+ vList = [direction[0], direction[1], direction[2]]
1008
+ except:
1009
+ raise Exception("Face.FacingToward - Error: Could not get the vector from the input direction")
1010
+ if asVertex:
1011
+ dV = [vList[0]-cList[0], vList[1]-cList[1], vList[2]-cList[2]]
1012
+ else:
1013
+ dV = vList
1014
+ uV = Vector.Normalize(dV)
1015
+ dot = sum([i*j for (i, j) in zip(uV, faceNormal)])
1016
+ if dot < tolerance:
1017
+ return False
1018
+ return True
1019
+
1020
+ @staticmethod
1021
+ def Harmonize(face: topologic.Face, tolerance: float = 0.0001) -> topologic.Face:
1022
+ """
1023
+ Returns a harmonized version of the input face such that the *u* and *v* origins are always in the upperleft corner.
1024
+
1025
+ Parameters
1026
+ ----------
1027
+ face : topologic.Face
1028
+ The input face.
1029
+ tolerance : float , optional
1030
+ The desired tolerance. The default is 0.0001.
1031
+
1032
+ Returns
1033
+ -------
1034
+ topologic.Face
1035
+ The harmonized face.
1036
+
1037
+ """
1038
+ from topologicpy.Vertex import Vertex
1039
+ from topologicpy.Wire import Wire
1040
+ from topologicpy.Topology import Topology
1041
+ from topologicpy.Dictionary import Dictionary
1042
+
1043
+ if not isinstance(face, topologic.Face):
1044
+ print("Face.Harmonize - Error: The input face parameter is not a valid face. Returning None.")
1045
+ return None
1046
+ normal = Face.Normal(face)
1047
+ origin = Topology.Centroid(face)
1048
+ flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1049
+ world_origin = Vertex.Origin()
1050
+ vertices = Wire.Vertices(Face.ExternalBoundary(flatFace))
1051
+ harmonizedEB = Wire.ByVertices(vertices)
1052
+ internalBoundaries = Face.InternalBoundaries(flatFace)
1053
+ harmonizedIB = []
1054
+ for ib in internalBoundaries:
1055
+ ibVertices = Wire.Vertices(ib)
1056
+ harmonizedIB.append(Wire.ByVertices(ibVertices))
1057
+ harmonizedFace = Face.ByWires(harmonizedEB, harmonizedIB, tolerance=tolerance)
1058
+ harmonizedFace = Topology.Unflatten(harmonizedFace, origin=origin, direction=normal)
1059
+ return harmonizedFace
1060
+
1061
+ @staticmethod
1062
+ def InteriorAngles(face: topologic.Face, includeInternalBoundaries: bool = False, mantissa: int = 6) -> list:
1063
+ """
1064
+ Returns the interior angles of the input face in degrees. The face must be planar.
1065
+
1066
+ Parameters
1067
+ ----------
1068
+ face : topologic.Face
1069
+ The input face.
1070
+ includeInternalBoundaries : bool , optional
1071
+ If set to True and if the face has internal boundaries (holes), the returned list will be a nested list where the first list is the list
1072
+ of interior angles of the external boundary and the second list will contain lists of the interior angles of each of the
1073
+ internal boundaries (holes). For example: [[90,90,90,90], [[90,90,90,90],[60,60,60]]]. If not, the returned list will be
1074
+ a simple list of interior angles of the external boundary. For example: [90,90,90,90]. Please note that that the interior angles of the
1075
+ internal boundaries are considered to be those interior to the original face. Thus, they are exterior to the internal boundary.
1076
+ mantissa : int , optional
1077
+ The desired length of the mantissa. The default is 6.
1078
+ Returns
1079
+ -------
1080
+ list
1081
+ The list of interior angles.
1082
+ """
1083
+
1084
+ from topologicpy.Wire import Wire
1085
+
1086
+ if not isinstance(face, topologic.Face):
1087
+ print("Face.InteriorAngles - Error: The input face parameter is not a valid face. Returning None.")
1088
+ return None
1089
+ eb = Face.ExternalBoundary(face)
1090
+ return_list = Wire.InteriorAngles(eb, mantissa=mantissa)
1091
+ if includeInternalBoundaries:
1092
+ internal_boundaries = Face.InternalBoundaries(face)
1093
+ ib_i_a_list = []
1094
+ if len(internal_boundaries) > 0:
1095
+ for ib in internal_boundaries:
1096
+ ib_interior_angles = Wire.ExteriorAngles(ib, mantissa=mantissa)
1097
+ ib_i_a_list.append(ib_interior_angles)
1098
+ if len(ib_i_a_list) > 0:
1099
+ return_list = [return_list]+[ib_i_a_list]
1100
+ return return_list
1101
+
1102
+ @staticmethod
1103
+ def InternalBoundaries(face: topologic.Face) -> list:
1104
+ """
1105
+ Returns the internal boundaries (closed wires) of the input face.
1106
+
1107
+ Parameters
1108
+ ----------
1109
+ face : topologic.Face
1110
+ The input face.
1111
+
1112
+ Returns
1113
+ -------
1114
+ list
1115
+ The list of internal boundaries (closed wires).
1116
+
1117
+ """
1118
+ if not isinstance(face, topologic.Face):
1119
+ return None
1120
+ wires = []
1121
+ _ = face.InternalBoundaries(wires)
1122
+ return list(wires)
1123
+
1124
+ @staticmethod
1125
+ def InternalVertex(face: topologic.Face, tolerance: float = 0.0001) -> topologic.Vertex:
1126
+ """
1127
+ Creates a vertex guaranteed to be inside the input face.
1128
+
1129
+ Parameters
1130
+ ----------
1131
+ face : topologic.Face
1132
+ The input face.
1133
+ tolerance : float , optional
1134
+ The desired tolerance. The default is 0.0001.
1135
+
1136
+ Returns
1137
+ -------
1138
+ topologic.Vertex
1139
+ The created vertex.
1140
+
1141
+ """
1142
+ from topologicpy.Vertex import Vertex
1143
+ from topologicpy.Topology import Topology
1144
+ if not isinstance(face, topologic.Face):
1145
+ return None
1146
+ v = Topology.Centroid(face)
1147
+ if Vertex.IsInternal(v, face, tolerance=tolerance):
1148
+ return v
1149
+ l = [0.4,0.6,0.3,0.7,0.2,0.8,0.1,0.9]
1150
+ for u in l:
1151
+ for v in l:
1152
+ v = Face.VertexByParameters(face, u, v)
1153
+ if Vertex.IsInternal(v, face, tolerance=tolerance):
1154
+ return v
1155
+ v = topologic.FaceUtility.InternalVertex(face, tolerance)
1156
+ return v
1157
+
1158
+ @staticmethod
1159
+ def Invert(face: topologic.Face, tolerance: float = 0.0001) -> topologic.Face:
1160
+ """
1161
+ Creates a face that is an inverse (mirror) of the input face.
1162
+
1163
+ Parameters
1164
+ ----------
1165
+ face : topologic.Face
1166
+ The input face.
1167
+ tolerance : float , optional
1168
+ The desired tolerance. The default is 0.0001.
1169
+
1170
+ Returns
1171
+ -------
1172
+ topologic.Face
1173
+ The inverted face.
1174
+
1175
+ """
1176
+ from topologicpy.Wire import Wire
1177
+
1178
+ if not isinstance(face, topologic.Face):
1179
+ return None
1180
+ eb = Face.ExternalBoundary(face)
1181
+ vertices = Wire.Vertices(eb)
1182
+ vertices.reverse()
1183
+ inverted_wire = Wire.ByVertices(vertices)
1184
+ internal_boundaries = Face.InternalBoundaries(face)
1185
+ if not internal_boundaries:
1186
+ inverted_face = Face.ByWire(inverted_wire, tolerance=tolerance)
1187
+ else:
1188
+ inverted_face = Face.ByWires(inverted_wire, internal_boundaries, tolerance=tolerance)
1189
+ return inverted_face
1190
+
1191
+ @staticmethod
1192
+ def IsCoplanar(faceA: topologic.Face, faceB: topologic.Face, tolerance: float = 0.0001) -> bool:
1193
+ """
1194
+ Returns True if the two input faces are coplanar. Returns False otherwise.
1195
+
1196
+ Parameters
1197
+ ----------
1198
+ faceA : topologic.Face
1199
+ The first input face.
1200
+ faceB : topologic.Face
1201
+ The second input face
1202
+ tolerance : float , optional
1203
+ The desired tolerance. The deafault is 0.0001.
1204
+
1205
+ Raises
1206
+ ------
1207
+ Exception
1208
+ Raises an exception if the angle between the two input faces cannot be determined.
1209
+
1210
+ Returns
1211
+ -------
1212
+ bool
1213
+ True if the two input faces are coplanar. False otherwise.
1214
+
1215
+ """
1216
+ if not isinstance(faceA, topologic.Face):
1217
+ print("Face.IsInide - Error: The input faceA parameter is not a valid topologic face. Returning None.")
1218
+ return None
1219
+ if not isinstance(faceB, topologic.Face):
1220
+ print("Face.IsInide - Error: The input faceB parameter is not a valid topologic face. Returning None.")
1221
+ return None
1222
+ dirA = Face.NormalAtParameters(faceA, 0.5, 0.5, "xyz", 3)
1223
+ dirB = Face.NormalAtParameters(faceB, 0.5, 0.5, "xyz", 3)
1224
+ return Vector.IsCollinear(dirA, dirB)
1225
+
1226
+ @staticmethod
1227
+ def MedialAxis(face: topologic.Face, resolution: int = 0, externalVertices: bool = False, internalVertices: bool = False, toLeavesOnly: bool = False, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Wire:
1228
+ """
1229
+ Returns a wire representing an approximation of the medial axis of the input topology. See https://en.wikipedia.org/wiki/Medial_axis.
1230
+
1231
+ Parameters
1232
+ ----------
1233
+ face : topologic.Face
1234
+ The input face.
1235
+ resolution : int , optional
1236
+ The desired resolution of the solution (range is 0: standard resolution to 10: high resolution). This determines the density of the sampling along each edge. The default is 0.
1237
+ externalVertices : bool , optional
1238
+ If set to True, the external vertices of the face will be connected to the nearest vertex on the medial axis. The default is False.
1239
+ internalVertices : bool , optional
1240
+ If set to True, the internal vertices of the face will be connected to the nearest vertex on the medial axis. The default is False.
1241
+ toLeavesOnly : bool , optional
1242
+ If set to True, the vertices of the face will be connected to the nearest vertex on the medial axis only if this vertex is a leaf (end point). Otherwise, it will connect to any nearest vertex. The default is False.
1243
+ angTolerance : float , optional
1244
+ The desired angular tolerance in degrees for removing collinear edges. The default is 0.1.
1245
+ tolerance : float , optional
1246
+ The desired tolerance. The default is 0.0001.
1247
+
1248
+ Returns
1249
+ -------
1250
+ topologic.Wire
1251
+ The medial axis of the input face.
1252
+
1253
+ """
1254
+ from topologicpy.Vertex import Vertex
1255
+ from topologicpy.Edge import Edge
1256
+ from topologicpy.Wire import Wire
1257
+ from topologicpy.Shell import Shell
1258
+ from topologicpy.Cluster import Cluster
1259
+ from topologicpy.Topology import Topology
1260
+ from topologicpy.Dictionary import Dictionary
1261
+
1262
+ def touchesEdge(vertex,edges, tolerance=0.0001):
1263
+ if not isinstance(vertex, topologic.Vertex):
1264
+ return False
1265
+ for edge in edges:
1266
+ u = Edge.ParameterAtVertex(edge, vertex, mantissa=6)
1267
+ if not u:
1268
+ continue
1269
+ if 0<u<1:
1270
+ return True
1271
+ return False
1272
+
1273
+ # Flatten the input face
1274
+ origin = Topology.Centroid(face)
1275
+ normal = Face.Normal(face)
1276
+ flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1277
+
1278
+ # Create a Vertex at the world's origin (0, 0, 0)
1279
+ world_origin = Vertex.Origin()
1280
+
1281
+ faceEdges = Face.Edges(flatFace)
1282
+ vertices = []
1283
+ resolution = 10 - resolution
1284
+ resolution = min(max(resolution, 1), 10)
1285
+ for e in faceEdges:
1286
+ for n in range(resolution, 100, resolution):
1287
+ vertices.append(Edge.VertexByParameter(e,n*0.01))
1288
+
1289
+ voronoi = Shell.Voronoi(vertices=vertices, face=flatFace)
1290
+ voronoiEdges = Shell.Edges(voronoi)
1291
+
1292
+ medialAxisEdges = []
1293
+ for e in voronoiEdges:
1294
+ sv = Edge.StartVertex(e)
1295
+ ev = Edge.EndVertex(e)
1296
+ svTouchesEdge = touchesEdge(sv, faceEdges, tolerance=tolerance)
1297
+ evTouchesEdge = touchesEdge(ev, faceEdges, tolerance=tolerance)
1298
+ if not svTouchesEdge and not evTouchesEdge:
1299
+ medialAxisEdges.append(e)
1300
+
1301
+ extBoundary = Face.ExternalBoundary(flatFace)
1302
+ extVertices = Wire.Vertices(extBoundary)
1303
+
1304
+ intBoundaries = Face.InternalBoundaries(flatFace)
1305
+ intVertices = []
1306
+ for ib in intBoundaries:
1307
+ intVertices = intVertices+Wire.Vertices(ib)
1308
+
1309
+ theVertices = []
1310
+ if internalVertices:
1311
+ theVertices = theVertices+intVertices
1312
+ if externalVertices:
1313
+ theVertices = theVertices+extVertices
1314
+
1315
+ tempWire = Topology.SelfMerge(Cluster.ByTopologies(medialAxisEdges), tolerance=tolerance)
1316
+ if isinstance(tempWire, topologic.Wire) and angTolerance > 0:
1317
+ tempWire = Wire.RemoveCollinearEdges(tempWire, angTolerance=angTolerance)
1318
+ medialAxisEdges = Wire.Edges(tempWire)
1319
+ for v in theVertices:
1320
+ nv = Vertex.NearestVertex(v, tempWire, useKDTree=False)
1321
+
1322
+ if isinstance(nv, topologic.Vertex):
1323
+ if toLeavesOnly:
1324
+ adjVertices = Topology.AdjacentTopologies(nv, tempWire)
1325
+ if len(adjVertices) < 2:
1326
+ medialAxisEdges.append(Edge.ByVertices([nv, v], tolerance=tolerance))
1327
+ else:
1328
+ medialAxisEdges.append(Edge.ByVertices([nv, v], tolerance=tolerance))
1329
+ medialAxis = Topology.SelfMerge(Cluster.ByTopologies(medialAxisEdges), tolerance=tolerance)
1330
+ if isinstance(medialAxis, topologic.Wire) and angTolerance > 0:
1331
+ medialAxis = Topology.RemoveCollinearEdges(medialAxis, angTolerance=angTolerance)
1332
+ medialAxis = Topology.Unflatten(medialAxis, origin=origin,direction=normal)
1333
+ return medialAxis
1334
+
1335
+ @staticmethod
1336
+ def Normal(face: topologic.Face, outputType: str = "xyz", mantissa: int = 6) -> list:
1337
+ """
1338
+ Returns the normal vector to the input face. A normal vector of a face is a vector perpendicular to it.
1339
+
1340
+ Parameters
1341
+ ----------
1342
+ face : topologic.Face
1343
+ The input face.
1344
+ outputType : string , optional
1345
+ The string defining the desired output. This can be any subset or permutation of "xyz". It is case insensitive. The default is "xyz".
1346
+ mantissa : int , optional
1347
+ The desired length of the mantissa. The default is 6.
1348
+
1349
+ Returns
1350
+ -------
1351
+ list
1352
+ The normal vector to the input face. This is computed at the approximate center of the face.
1353
+
1354
+ """
1355
+ return Face.NormalAtParameters(face, u=0.5, v=0.5, outputType=outputType, mantissa=mantissa)
1356
+
1357
+ @staticmethod
1358
+ def NormalAtParameters(face: topologic.Face, u: float = 0.5, v: float = 0.5, outputType: str = "xyz", mantissa: int = 6) -> list:
1359
+ """
1360
+ Returns the normal vector to the input face. A normal vector of a face is a vector perpendicular to it.
1361
+
1362
+ Parameters
1363
+ ----------
1364
+ face : topologic.Face
1365
+ The input face.
1366
+ u : float , optional
1367
+ The *u* parameter at which to compute the normal to the input face. The default is 0.5.
1368
+ v : float , optional
1369
+ The *v* parameter at which to compute the normal to the input face. The default is 0.5.
1370
+ outputType : string , optional
1371
+ The string defining the desired output. This can be any subset or permutation of "xyz". It is case insensitive. The default is "xyz".
1372
+ mantissa : int , optional
1373
+ The desired length of the mantissa. The default is 6.
1374
+
1375
+ Returns
1376
+ -------
1377
+ list
1378
+ The normal vector to the input face.
1379
+
1380
+ """
1381
+ returnResult = []
1382
+ try:
1383
+ coords = topologic.FaceUtility.NormalAtParameters(face, u, v)
1384
+ x = round(coords[0], mantissa)
1385
+ y = round(coords[1], mantissa)
1386
+ z = round(coords[2], mantissa)
1387
+ outputType = list(outputType.lower())
1388
+ for axis in outputType:
1389
+ if axis == "x":
1390
+ returnResult.append(x)
1391
+ elif axis == "y":
1392
+ returnResult.append(y)
1393
+ elif axis == "z":
1394
+ returnResult.append(z)
1395
+ except:
1396
+ returnResult = None
1397
+ return returnResult
1398
+
1399
+ @staticmethod
1400
+ def NormalEdge(face: topologic.Face, length: float = 1.0, tolerance: float = 0.0001, silent: bool = False) -> topologic.Edge:
1401
+ """
1402
+ Returns the normal vector to the input face as an edge with the desired input length. A normal vector of a face is a vector perpendicular to it.
1403
+
1404
+ Parameters
1405
+ ----------
1406
+ face : topologic.Face
1407
+ The input face.
1408
+ length : float , optional
1409
+ The desired length of the normal edge. The default is 1.
1410
+ tolerance : float , optional
1411
+ The desired tolerance. The default is 0.0001.
1412
+
1413
+ Returns
1414
+ -------
1415
+ topologic.Edge
1416
+ The created normal edge to the input face. This is computed at the approximate center of the face.
1417
+
1418
+ """
1419
+ from topologicpy.Edge import Edge
1420
+
1421
+ if not isinstance(face, topologic.Face):
1422
+ if not silent:
1423
+ print("Face.NormalEdge - Error: The input face parameter is not a valid face. Retuning None.")
1424
+ return None
1425
+ if length < tolerance:
1426
+ if not silent:
1427
+ print("Face.NormalEdge - Error: The input length parameter is less than the input tolerance. Retuning None.")
1428
+ return None
1429
+ iv = Face.InternalVertex(face)
1430
+ u, v = Face.VertexParameters(face, iv)
1431
+ vec = Face.NormalAtParameters(face, u=u, v=v)
1432
+ ev = Topology.TranslateByDirectionDistance(iv, vec, length)
1433
+ return Edge.ByVertices([iv, ev], tolerance=tolerance, silent=silent)
1434
+
1435
+ @staticmethod
1436
+ def NormalEdgeAtParameters(face: topologic.Face, u: float = 0.5, v: float = 0.5, length: float = 1.0, tolerance: float = 0.0001) -> topologic.Edge:
1437
+ """
1438
+ Returns the normal vector to the input face as an edge with the desired input length. A normal vector of a face is a vector perpendicular to it.
1439
+
1440
+ Parameters
1441
+ ----------
1442
+ face : topologic.Face
1443
+ The input face.
1444
+ u : float , optional
1445
+ The *u* parameter at which to compute the normal to the input face. The default is 0.5.
1446
+ v : float , optional
1447
+ The *v* parameter at which to compute the normal to the input face. The default is 0.5.
1448
+ length : float , optional
1449
+ The desired length of the normal edge. The default is 1.
1450
+ tolerance : float , optional
1451
+ The desired tolerance. The default is 0.0001.
1452
+
1453
+ Returns
1454
+ -------
1455
+ topologic.Edge
1456
+ The created normal edge to the input face. This is computed at the approximate center of the face.
1457
+
1458
+ """
1459
+ from topologicpy.Edge import Edge
1460
+ from topologicpy.Topology import Topology
1461
+ if not isinstance(face, topologic.Face):
1462
+ return None
1463
+ sv = Face.VertexByParameters(face=face, u=u, v=v)
1464
+ vec = Face.NormalAtParameters(face, u=u, v=v)
1465
+ ev = Topology.TranslateByDirectionDistance(sv, vec, length)
1466
+ return Edge.ByVertices([sv, ev], tolerance=tolerance, silent=True)
1467
+
1468
+ @staticmethod
1469
+ def PlaneEquation(face: topologic.Face, mantissa: int = 6) -> dict:
1470
+ """
1471
+ Returns the a, b, c, d coefficients of the plane equation of the input face. The input face is assumed to be planar.
1472
+
1473
+ Parameters
1474
+ ----------
1475
+ face : topologic.Face
1476
+ The input face.
1477
+
1478
+ Returns
1479
+ -------
1480
+ dict
1481
+ The dictionary containing the coefficients of the plane equation. The keys of the dictionary are: ["a", "b", "c", "d"].
1482
+
1483
+ """
1484
+ from topologicpy.Topology import Topology
1485
+ from topologicpy.Vertex import Vertex
1486
+ import random
1487
+ import time
1488
+
1489
+ if not isinstance(face, topologic.Face):
1490
+ print("Face.PlaneEquation - Error: The input face is not a valid topologic face. Returning None.")
1491
+ return None
1492
+ vertices = Topology.Vertices(face)
1493
+ if len(vertices) < 3:
1494
+ print("Face.PlaneEquation - Error: The input face has less than 3 vertices. Returning None.")
1495
+ return None
1496
+ return Vertex.PlaneEquation(vertices, mantissa=mantissa)
1497
+
1498
+ @staticmethod
1499
+ def Planarize(face: topologic.Face, origin: topologic.Vertex = None,
1500
+ tolerance: float = 0.0001) -> topologic.Face:
1501
+ """
1502
+ Planarizes the input face such that its center of mass is located at the input origin and its normal is pointed in the input direction.
1503
+
1504
+ Parameters
1505
+ ----------
1506
+ face : topologic.Face
1507
+ The input face.
1508
+ origin : topologic.Vertex , optional
1509
+ The desired vertex to use as the origin of the plane to project the face unto. If set to None, the centroidof the input face is used. The default is None.
1510
+ tolerance : float , optional
1511
+ The desired tolerance. The default is 0.0001.
1512
+
1513
+ Returns
1514
+ -------
1515
+ topologic.Face
1516
+ The planarized face.
1517
+
1518
+ """
1519
+
1520
+ from topologicpy.Wire import Wire
1521
+ from topologicpy.Topology import Topology
1522
+
1523
+ if not isinstance(face, topologic.Face):
1524
+ return None
1525
+ if not isinstance(origin, topologic.Vertex):
1526
+ origin = Topology.Centroid(face)
1527
+ eb = Face.ExternalBoundary(face)
1528
+ plan_eb = Wire.Planarize(eb, origin=origin)
1529
+ ib_list = Face.InternalBoundaries(face)
1530
+ plan_ib_list = []
1531
+ for ib in ib_list:
1532
+ plan_ib_list.append(Wire.Planarize(ib, origin=origin))
1533
+ plan_face = Face.ByWires(plan_eb, plan_ib_list)
1534
+ return plan_face
1535
+
1536
+ @staticmethod
1537
+ def Project(faceA: topologic.Face, faceB: topologic.Face, direction : list = None,
1538
+ mantissa: int = 6, tolerance: float = 0.0001) -> topologic.Face:
1539
+ """
1540
+ Creates a projection of the first input face unto the second input face.
1541
+
1542
+ Parameters
1543
+ ----------
1544
+ faceA : topologic.Face
1545
+ The face to be projected.
1546
+ faceB : topologic.Face
1547
+ The face unto which the first input face will be projected.
1548
+ direction : list, optional
1549
+ The vector direction of the projection. If None, the reverse vector of the receiving face normal will be used. The default is None.
1550
+ mantissa : int , optional
1551
+ The desired length of the mantissa. The default is 6.
1552
+ tolerance : float , optional
1553
+ The desired tolerance. The default is 0.0001.
1554
+
1555
+ Returns
1556
+ -------
1557
+ topologic.Face
1558
+ The projected Face.
1559
+
1560
+ """
1561
+
1562
+ from topologicpy.Wire import Wire
1563
+
1564
+ if not faceA:
1565
+ return None
1566
+ if not isinstance(faceA, topologic.Face):
1567
+ return None
1568
+ if not faceB:
1569
+ return None
1570
+ if not isinstance(faceB, topologic.Face):
1571
+ return None
1572
+
1573
+ eb = faceA.ExternalBoundary()
1574
+ ib_list = []
1575
+ _ = faceA.InternalBoundaries(ib_list)
1576
+ p_eb = Wire.Project(wire=eb, face = faceB, direction=direction, mantissa=mantissa, tolerance=tolerance)
1577
+ p_ib_list = []
1578
+ for ib in ib_list:
1579
+ temp_ib = Wire.Project(wire=ib, face = faceB, direction=direction, mantissa=mantissa, tolerance=tolerance)
1580
+ if temp_ib:
1581
+ p_ib_list.append(temp_ib)
1582
+ return Face.ByWires(p_eb, p_ib_list, tolerance=tolerance)
1583
+
1584
+ @staticmethod
1585
+ def RectangleByPlaneEquation(origin: topologic.Vertex = None, width: float = 1.0, length: float = 1.0, placement: str = "center", equation: dict = None, tolerance: float = 0.0001) -> topologic.Face:
1586
+ from topologicpy.Vertex import Vertex
1587
+ # Extract coefficients of the plane equation
1588
+ a = equation['a']
1589
+ b = equation['b']
1590
+ c = equation['c']
1591
+ d = equation['d']
1592
+
1593
+ # Calculate the normal vector of the plane
1594
+ direction = np.array([a, b, c], dtype=float)
1595
+ direction /= np.linalg.norm(direction)
1596
+ direction = [x for x in direction]
1597
+
1598
+ return Face.Rectangle(origin=origin, width=width, length=length, direction = direction, placement=placement, tolerance=tolerance)
1599
+
1600
+ @staticmethod
1601
+ def Rectangle(origin: topologic.Vertex = None, width: float = 1.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1602
+ """
1603
+ Creates a rectangle.
1604
+
1605
+ Parameters
1606
+ ----------
1607
+ origin : topologic.Vertex, optional
1608
+ The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0).
1609
+ width : float , optional
1610
+ The width of the rectangle. The default is 1.0.
1611
+ length : float , optional
1612
+ The length of the rectangle. The default is 1.0.
1613
+ direction : list , optional
1614
+ The vector representing the up direction of the rectangle. The default is [0, 0, 1].
1615
+ placement : str , optional
1616
+ The description of the placement of the origin of the rectangle. This can be "center", "lowerleft", "upperleft", "lowerright", "upperright". It is case insensitive. The default is "center".
1617
+ tolerance : float , optional
1618
+ The desired tolerance. The default is 0.0001.
1619
+
1620
+ Returns
1621
+ -------
1622
+ topologic.Face
1623
+ The created face.
1624
+
1625
+ """
1626
+ from topologicpy.Wire import Wire
1627
+
1628
+ wire = Wire.Rectangle(origin=origin, width=width, length=length, direction=direction, placement=placement, tolerance=tolerance)
1629
+ if not isinstance(wire, topologic.Wire):
1630
+ print("Face.Rectangle - Error: Could not create the base wire for the rectangle. Returning None.")
1631
+ return None
1632
+ return Face.ByWire(wire, tolerance=tolerance)
1633
+
1634
+ @staticmethod
1635
+ def RemoveCollinearEdges(face: topologic.Face, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Wire:
1636
+ """
1637
+ Removes any collinear edges in the input face.
1638
+
1639
+ Parameters
1640
+ ----------
1641
+ face : topologic.Face
1642
+ The input face.
1643
+ angTolerance : float , optional
1644
+ The desired angular tolerance. The default is 0.1.
1645
+ tolerance : float , optional
1646
+ The desired tolerance. The default is 0.0001.
1647
+
1648
+ Returns
1649
+ -------
1650
+ topologic.Face
1651
+ The created face without any collinear edges.
1652
+
1653
+ """
1654
+ from topologicpy.Wire import Wire
1655
+
1656
+ if not isinstance(face, topologic.Face):
1657
+ print("Face.RemoveCollinearEdges - Error: The input face parameter is not a valid face. Returning None.")
1658
+ return None
1659
+ eb = Wire.RemoveCollinearEdges(Face.Wire(face), angTolerance=angTolerance, tolerance=tolerance)
1660
+ ib = [Wire.RemoveCollinearEdges(w, angTolerance=angTolerance, tolerance=tolerance) for w in Face.InternalBoundaries(face)]
1661
+ return Face.ByWires(eb, ib)
1662
+
1663
+ @staticmethod
1664
+ def Skeleton(face, tolerance=0.001):
1665
+ """
1666
+ Creates a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
1667
+ This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
1668
+
1669
+ Parameters
1670
+ ----------
1671
+ face : topologic.Face
1672
+ The input face.
1673
+ tolerance : float , optional
1674
+ The desired tolerance. The default is 0.001. (This is set to a larger number than the usual 0.0001 as it was found to work better)
1675
+
1676
+ Returns
1677
+ -------
1678
+ topologic.Wire
1679
+ The created straight skeleton.
1680
+
1681
+ """
1682
+ from topologicpy.Wire import Wire
1683
+ if not isinstance(face, topologic.Face):
1684
+ print("Face.Skeleton - Error: The input face is not a valid topologic face. Retruning None.")
1685
+ return None
1686
+ return Wire.Skeleton(face, tolerance=tolerance)
1687
+
1688
+ @staticmethod
1689
+ def Square(origin: topologic.Vertex = None, size: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1690
+ """
1691
+ Creates a square.
1692
+
1693
+ Parameters
1694
+ ----------
1695
+ origin : topologic.Vertex , optional
1696
+ The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0).
1697
+ size : float , optional
1698
+ The size of the square. The default is 1.0.
1699
+ direction : list , optional
1700
+ The vector representing the up direction of the square. The default is [0, 0, 1].
1701
+ placement : str , optional
1702
+ The description of the placement of the origin of the square. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
1703
+ tolerance : float , optional
1704
+ The desired tolerance. The default is 0.0001.
1705
+
1706
+ Returns
1707
+ -------
1708
+ topologic.Face
1709
+ The created square.
1710
+
1711
+ """
1712
+ return Face.Rectangle(origin=origin, width=size, length=size, direction=direction, placement=placement, tolerance=tolerance)
1713
+
1714
+ @staticmethod
1715
+ def Star(origin: topologic.Vertex = None, radiusA: float = 1.0, radiusB: float = 0.4, rays: int = 5, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1716
+ """
1717
+ Creates a star.
1718
+
1719
+ Parameters
1720
+ ----------
1721
+ origin : topologic.Vertex, optional
1722
+ The location of the origin of the star. The default is None which results in the star being placed at (0, 0, 0).
1723
+ radiusA : float , optional
1724
+ The outer radius of the star. The default is 1.0.
1725
+ radiusB : float , optional
1726
+ The outer radius of the star. The default is 0.4.
1727
+ rays : int , optional
1728
+ The number of star rays. The default is 5.
1729
+ direction : list , optional
1730
+ The vector representing the up direction of the star. The default is [0, 0, 1].
1731
+ placement : str , optional
1732
+ The description of the placement of the origin of the star. This can be "center", "lowerleft", "upperleft", "lowerright", or "upperright". It is case insensitive. The default is "center".
1733
+ tolerance : float , optional
1734
+ The desired tolerance. The default is 0.0001.
1735
+
1736
+ Returns
1737
+ -------
1738
+ topologic.Face
1739
+ The created face.
1740
+
1741
+ """
1742
+ from topologicpy.Wire import Wire
1743
+ wire = Wire.Star(origin=origin, radiusA=radiusA, radiusB=radiusB, rays=rays, direction=direction, placement=placement, tolerance=tolerance)
1744
+ if not isinstance(wire, topologic.Wire):
1745
+ print("Face.Rectangle - Error: Could not create the base wire for the star. Returning None.")
1746
+ return None
1747
+ return Face.ByWire(wire, tolerance=tolerance)
1748
+
1749
+ @staticmethod
1750
+ def Trapezoid(origin: topologic.Vertex = None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Face:
1751
+ """
1752
+ Creates a trapezoid.
1753
+
1754
+ Parameters
1755
+ ----------
1756
+ origin : topologic.Vertex, optional
1757
+ The location of the origin of the trapezoid. The default is None which results in the trapezoid being placed at (0, 0, 0).
1758
+ widthA : float , optional
1759
+ The width of the bottom edge of the trapezoid. The default is 1.0.
1760
+ widthB : float , optional
1761
+ The width of the top edge of the trapezoid. The default is 0.75.
1762
+ offsetA : float , optional
1763
+ The offset of the bottom edge of the trapezoid. The default is 0.0.
1764
+ offsetB : float , optional
1765
+ The offset of the top edge of the trapezoid. The default is 0.0.
1766
+ length : float , optional
1767
+ The length of the trapezoid. The default is 1.0.
1768
+ direction : list , optional
1769
+ The vector representing the up direction of the trapezoid. The default is [0, 0, 1].
1770
+ placement : str , optional
1771
+ The description of the placement of the origin of the trapezoid. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1772
+ tolerance : float , optional
1773
+ The desired tolerance. The default is 0.0001.
1774
+
1775
+ Returns
1776
+ -------
1777
+ topologic.Face
1778
+ The created trapezoid.
1779
+
1780
+ """
1781
+ from topologicpy.Wire import Wire
1782
+ wire = Wire.Trapezoid(origin=origin, widthA=widthA, widthB=widthB, offsetA=offsetA, offsetB=offsetB, length=length, direction=direction, placement=placement, tolerance=tolerance)
1783
+ if not isinstance(wire, topologic.Wire):
1784
+ print("Face.Rectangle - Error: Could not create the base wire for the trapezoid. Returning None.")
1785
+ return None
1786
+ return Face.ByWire(wire, tolerance=tolerance)
1787
+
1788
+ @staticmethod
1789
+ def Triangulate(face:topologic.Face, mode: int = 0, meshSize: float = None, tolerance: float = 0.0001) -> list:
1790
+ """
1791
+ Triangulates the input face and returns a list of faces.
1792
+
1793
+ Parameters
1794
+ ----------
1795
+ face : topologic.Face
1796
+ The input face.
1797
+ tolerance : float , optional
1798
+ The desired tolerance. The default is 0.0001.
1799
+ mode : int , optional
1800
+ The desired mode of meshing algorithm. Several options are available:
1801
+ 0: Classic
1802
+ 1: MeshAdapt
1803
+ 3: Initial Mesh Only
1804
+ 5: Delaunay
1805
+ 6: Frontal-Delaunay
1806
+ 7: BAMG
1807
+ 8: Fontal-Delaunay for Quads
1808
+ 9: Packing of Parallelograms
1809
+ All options other than 0 (Classic) use the gmsh library. See https://gmsh.info/doc/texinfo/gmsh.html#Mesh-options
1810
+ WARNING: The options that use gmsh can be very time consuming and can create very heavy geometry.
1811
+ meshSize : float , optional
1812
+ The desired size of the mesh when using the "mesh" option. If set to None, it will be
1813
+ calculated automatically and set to 10% of the overall size of the face.
1814
+
1815
+ Returns
1816
+ -------
1817
+ list
1818
+ The list of triangles of the input face.
1819
+
1820
+ """
1821
+ from topologicpy.Wire import Wire
1822
+ from topologicpy.Topology import Topology
1823
+
1824
+ # This function was contributed by Yidan Xue.
1825
+ def generate_gmsh(face, mode="mesh", meshSize = None, tolerance = 0.0001):
1826
+ """
1827
+ Creates a gmsh of triangular meshes from the input face.
1828
+
1829
+ Parameters
1830
+ ----------
1831
+ face : topologic.Face
1832
+ The input face.
1833
+ meshSize : float , optional
1834
+ The desired mesh size.
1835
+ tolerance : float , optional
1836
+ The desired tolerance. The default is 0.0001.
1837
+
1838
+ Returns
1839
+ ----------
1840
+ topologic.Shell
1841
+ The shell of triangular meshes.
1842
+
1843
+ """
1844
+ import os
1845
+ import warnings
1846
+ try:
1847
+ import numpy as np
1848
+ except:
1849
+ print("Face.Triangulate - Warning: Installing required numpy library.")
1850
+ try:
1851
+ os.system("pip install numpy")
1852
+ except:
1853
+ os.system("pip install numpy --user")
1854
+ try:
1855
+ import numpy as np
1856
+ print("Face.Triangulate - Warning: numpy library installed correctly.")
1857
+ except:
1858
+ warnings.warn("Face.Triangulate - Error: Could not import numpy. Please try to install numpy manually. Returning None.")
1859
+ return None
1860
+ try:
1861
+ import gmsh
1862
+ except:
1863
+ print("Face.Triangulate - Warning: Installing required gmsh library.")
1864
+ try:
1865
+ os.system("pip install gmsh")
1866
+ except:
1867
+ os.system("pip install gmsh --user")
1868
+ try:
1869
+ import gmsh
1870
+ print("Face.Triangulate - Warning: gmsh library installed correctly.")
1871
+ except:
1872
+ warnings.warn("Face.Triangulate - Error: Could not import gmsh. Please try to install gmsh manually. Returning None.")
1873
+ return None
1874
+
1875
+ import topologic_core as topologic
1876
+ from topologicpy.Vertex import Vertex
1877
+ from topologicpy.Wire import Wire
1878
+ from topologicpy.Face import Face
1879
+
1880
+ if not isinstance(face, topologic.Face):
1881
+ print("Shell.ByMeshFace - Error: The input face parameter is not a valid face. Returning None.")
1882
+ return None
1883
+ if not meshSize:
1884
+ bounding_face = Face.BoundingRectangle(face)
1885
+ bounding_face_vertices = Face.Vertices(bounding_face)
1886
+ bounding_face_vertices_x = [Vertex.X(i) for i in bounding_face_vertices]
1887
+ bounding_face_vertices_y = [Vertex.Y(i) for i in bounding_face_vertices]
1888
+ width = max(bounding_face_vertices_x)-min(bounding_face_vertices_x)
1889
+ length = max(bounding_face_vertices_y)-min(bounding_face_vertices_y)
1890
+ meshSize = max([width,length])//10
1891
+
1892
+ gmsh.initialize()
1893
+ face_external_boundary = Face.ExternalBoundary(face)
1894
+ external_vertices = Wire.Vertices(face_external_boundary)
1895
+ external_vertex_number = len(external_vertices)
1896
+ for i in range(external_vertex_number):
1897
+ gmsh.model.geo.addPoint(Vertex.X(external_vertices[i]), Vertex.Y(external_vertices[i]), Vertex.Z(external_vertices[i]), meshSize, i+1)
1898
+ for i in range(external_vertex_number):
1899
+ if i < external_vertex_number-1:
1900
+ gmsh.model.geo.addLine(i+1, i+2, i+1)
1901
+ else:
1902
+ gmsh.model.geo.addLine(i+1, 1, i+1)
1903
+ gmsh.model.geo.addCurveLoop([i+1 for i in range(external_vertex_number)], 1)
1904
+ current_vertex_number = external_vertex_number
1905
+ current_edge_number = external_vertex_number
1906
+ current_wire_number = 1
1907
+
1908
+ face_internal_boundaries = Face.InternalBoundaries(face)
1909
+ if face_internal_boundaries:
1910
+ internal_face_number = len(face_internal_boundaries)
1911
+ for i in range(internal_face_number):
1912
+ face_internal_boundary = face_internal_boundaries[i]
1913
+ internal_vertices = Wire.Vertices(face_internal_boundary)
1914
+ internal_vertex_number = len(internal_vertices)
1915
+ for j in range(internal_vertex_number):
1916
+ gmsh.model.geo.addPoint(Vertex.X(internal_vertices[j]), Vertex.Y(internal_vertices[j]), Vertex.Z(internal_vertices[j]), meshSize, current_vertex_number+j+1)
1917
+ for j in range(internal_vertex_number):
1918
+ if j < internal_vertex_number-1:
1919
+ gmsh.model.geo.addLine(current_vertex_number+j+1, current_vertex_number+j+2, current_edge_number+j+1)
1920
+ else:
1921
+ gmsh.model.geo.addLine(current_vertex_number+j+1, current_vertex_number+1, current_edge_number+j+1)
1922
+ gmsh.model.geo.addCurveLoop([current_edge_number+i+1 for i in range(internal_vertex_number)], current_wire_number+1)
1923
+ current_vertex_number = current_vertex_number+internal_vertex_number
1924
+ current_edge_number = current_edge_number+internal_vertex_number
1925
+ current_wire_number = current_wire_number+1
1926
+
1927
+ gmsh.model.geo.addPlaneSurface([i+1 for i in range(current_wire_number)])
1928
+ gmsh.model.geo.synchronize()
1929
+ if mode not in [1,3,5,6,7,8,9]:
1930
+ mode = 6
1931
+ gmsh.option.setNumber("Mesh.Algorithm", mode)
1932
+ gmsh.model.mesh.generate(2) # For a 2D mesh
1933
+ nodeTags, nodeCoords, nodeParams = gmsh.model.mesh.getNodes(-1, -1)
1934
+ elemTypes, elemTags, elemNodeTags = gmsh.model.mesh.getElements(-1, -1)
1935
+ gmsh.finalize()
1936
+
1937
+ vertex_number = len(nodeTags)
1938
+ vertices = []
1939
+ for i in range(vertex_number):
1940
+ vertices.append(Vertex.ByCoordinates(nodeCoords[3*i],nodeCoords[3*i+1],nodeCoords[3*i+2]))
1941
+
1942
+ faces = []
1943
+ for n in range(len(elemTypes)):
1944
+ vn = elemTypes[n]+1
1945
+ et = elemTags[n]
1946
+ ent = elemNodeTags[n]
1947
+ if vn==3:
1948
+ for i in range(len(et)):
1949
+ face_vertices = []
1950
+ for j in range(vn):
1951
+ face_vertices.append(vertices[np.where(nodeTags==ent[i*vn+j])[0][0]])
1952
+ faces.append(Face.ByVertices(face_vertices))
1953
+ return faces
1954
+
1955
+ if not isinstance(face, topologic.Face):
1956
+ print("Face.Triangulate - Error: The input face parameter is not a valid face. Returning None.")
1957
+ return None
1958
+ vertices = Topology.Vertices(face)
1959
+ if len(vertices) == 3: # Already a triangle
1960
+ return [face]
1961
+ origin = Topology.Centroid(face)
1962
+ normal = Face.Normal(face)
1963
+ flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1964
+
1965
+ if mode == 0:
1966
+ shell_faces = []
1967
+ for i in range(0,5,1):
1968
+ try:
1969
+ _ = topologic.FaceUtility.Triangulate(flatFace, float(i)*0.1, shell_faces)
1970
+ break
1971
+ except:
1972
+ continue
1973
+ else:
1974
+ shell_faces = generate_gmsh(flatFace, mode = mode, meshSize = meshSize, tolerance = tolerance)
1975
+
1976
+ if len(shell_faces) < 1:
1977
+ return []
1978
+ finalFaces = []
1979
+ for f in shell_faces:
1980
+ f = Topology.Unflatten(f, origin=origin, direction=normal)
1981
+ if Face.Angle(face, f) > 90:
1982
+ wire = Face.ExternalBoundary(f)
1983
+ wire = Wire.Invert(wire)
1984
+ f = topologic.Face.ByExternalBoundary(wire)
1985
+ finalFaces.append(f)
1986
+ else:
1987
+ finalFaces.append(f)
1988
+ return finalFaces
1989
+
1990
+ @staticmethod
1991
+ def TrimByWire(face: topologic.Face, wire: topologic.Wire, reverse: bool = False) -> topologic.Face:
1992
+ """
1993
+ Trims the input face by the input wire.
1994
+
1995
+ Parameters
1996
+ ----------
1997
+ face : topologic.Face
1998
+ The input face.
1999
+ wire : topologic.Wire
2000
+ The input wire.
2001
+ reverse : bool , optional
2002
+ If set to True, the effect of the trim will be reversed. The default is False.
2003
+
2004
+ Returns
2005
+ -------
2006
+ topologic.Face
2007
+ The resulting trimmed face.
2008
+
2009
+ """
2010
+ if not isinstance(face, topologic.Face):
2011
+ return None
2012
+ if not isinstance(wire, topologic.Wire):
2013
+ return face
2014
+ trimmed_face = topologic.FaceUtility.TrimByWire(face, wire, False)
2015
+ if reverse:
2016
+ trimmed_face = face.Difference(trimmed_face)
2017
+ return trimmed_face
2018
+
2019
+ @staticmethod
2020
+ def VertexByParameters(face: topologic.Face, u: float = 0.5, v: float = 0.5) -> topologic.Vertex:
2021
+ """
2022
+ Creates a vertex at the *u* and *v* parameters of the input face.
2023
+
2024
+ Parameters
2025
+ ----------
2026
+ face : topologic.Face
2027
+ The input face.
2028
+ u : float , optional
2029
+ The *u* parameter of the input face. The default is 0.5.
2030
+ v : float , optional
2031
+ The *v* parameter of the input face. The default is 0.5.
2032
+
2033
+ Returns
2034
+ -------
2035
+ vertex : topologic vertex
2036
+ The created vertex.
2037
+
2038
+ """
2039
+ if not isinstance(face, topologic.Face):
2040
+ return None
2041
+ return topologic.FaceUtility.VertexAtParameters(face, u, v)
2042
+
2043
+ @staticmethod
2044
+ def VertexParameters(face: topologic.Face, vertex: topologic.Vertex, outputType: str = "uv", mantissa: int = 6) -> list:
2045
+ """
2046
+ Returns the *u* and *v* parameters of the input face at the location of the input vertex.
2047
+
2048
+ Parameters
2049
+ ----------
2050
+ face : topologic.Face
2051
+ The input face.
2052
+ vertex : topologic.Vertex
2053
+ The input vertex.
2054
+ outputType : string , optional
2055
+ The string defining the desired output. This can be any subset or permutation of "uv". It is case insensitive. The default is "uv".
2056
+ mantissa : int , optional
2057
+ The desired length of the mantissa. The default is 6.
2058
+
2059
+ Returns
2060
+ -------
2061
+ list
2062
+ The list of *u* and/or *v* as specified by the outputType input.
2063
+
2064
+ """
2065
+ if not isinstance(face, topologic.Face):
2066
+ return None
2067
+ if not isinstance(vertex, topologic.Vertex):
2068
+ return None
2069
+ params = topologic.FaceUtility.ParametersAtVertex(face, vertex)
2070
+ u = round(params[0], mantissa)
2071
+ v = round(params[1], mantissa)
2072
+ outputType = list(outputType.lower())
2073
+ returnResult = []
2074
+ for param in outputType:
2075
+ if param == "u":
2076
+ returnResult.append(u)
2077
+ elif param == "v":
2078
+ returnResult.append(v)
2079
+ return returnResult
2080
+
2081
+ @staticmethod
2082
+ def Vertices(face: topologic.Face) -> list:
2083
+ """
2084
+ Returns the vertices of the input face.
2085
+
2086
+ Parameters
2087
+ ----------
2088
+ face : topologic.Face
2089
+ The input face.
2090
+
2091
+ Returns
2092
+ -------
2093
+ list
2094
+ The list of vertices.
2095
+
2096
+ """
2097
+ if not isinstance(face, topologic.Face):
2098
+ return None
2099
+ vertices = []
2100
+ _ = face.Vertices(None, vertices)
2101
+ return vertices
2102
+
2103
+ @staticmethod
2104
+ def Wire(face: topologic.Face) -> topologic.Wire:
2105
+ """
2106
+ Returns the external boundary (closed wire) of the input face.
2107
+
2108
+ Parameters
2109
+ ----------
2110
+ face : topologic.Face
2111
+ The input face.
2112
+
2113
+ Returns
2114
+ -------
2115
+ topologic.Wire
2116
+ The external boundary of the input face.
2117
+
2118
+ """
2119
+ return face.ExternalBoundary()
2120
+
2121
+ @staticmethod
2122
+ def Wires(face: topologic.Face) -> list:
2123
+ """
2124
+ Returns the wires of the input face.
2125
+
2126
+ Parameters
2127
+ ----------
2128
+ face : topologic.Face
2129
+ The input face.
2130
+
2131
+ Returns
2132
+ -------
2133
+ list
2134
+ The list of wires.
2135
+
2136
+ """
2137
+ if not isinstance(face, topologic.Face):
2138
+ return None
2139
+ wires = []
2140
+ _ = face.Wires(None, wires)
2141
+ return wires