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/Shell.py CHANGED
@@ -1,1769 +1,1769 @@
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.Topology import Topology
19
- import math
20
- import os
21
- import warnings
22
-
23
- try:
24
- from tqdm.auto import tqdm
25
- except:
26
- print("Shell - Installing required tqdm library.")
27
- try:
28
- os.system("pip install tqdm")
29
- except:
30
- os.system("pip install tqdm --user")
31
- try:
32
- from tqdm.auto import tqdm
33
- print("Shell - tqdm library installed correctly.")
34
- except:
35
- warnings.warn("Shell - Error: Could not import tqdm.")
36
-
37
- try:
38
- from scipy.spatial import Delaunay
39
- from scipy.spatial import Voronoi
40
- except:
41
- print("Shell - Install required scipy library.")
42
- try:
43
- os.system("pip install scipy")
44
- except:
45
- os.system("pip install scipy --user")
46
- try:
47
- from scipy.spatial import Delaunay
48
- from scipy.spatial import Voronoi
49
- except:
50
- warnings.warn("Shell - Error: Could not import scipy.")
51
-
52
- class Shell(Topology):
53
- @staticmethod
54
- def ByDisjointFaces(externalBoundary, faces, maximumGap=0.5, mergeJunctions=False, threshold=0.5, uSides=1, vSides=1, transferDictionaries=False, tolerance=0.0001):
55
- """
56
- Creates a shell from an input list of disjointed faces. THIS IS STILL EXPERIMENTAL
57
-
58
- Parameters
59
- ----------
60
- externalBoundary : topologic.Face
61
- The input external boundary of the faces. This resembles a ribbon (face with hole) where its interior boundary touches the edges of the input list of faces.
62
- faces : list
63
- The input list of faces.
64
- maximumGap : float , optional
65
- The length of the maximum gap between the faces. The default is 0.5.
66
- mergeJunctions : bool , optional
67
- If set to True, the interior junctions are merged into a single vertex. Otherwise, diagonal edges are added to resolve transitions between different gap distances.
68
- threshold : float , optional
69
- The desired threshold under which vertices are merged into a single vertex. The default is 0.5.
70
- uSides : int , optional
71
- The desired number of sides along the X axis for the grid that subdivides the input faces to aid in processing. The default is 1.
72
- vSides : int , optional
73
- The desired number of sides along the Y axis for the grid that subdivides the input faces to aid in processing. The default is 1.
74
- transferDictionaries : bool, optional.
75
- If set to True, the dictionaries in the input list of faces are transfered to the faces of the resulting shell. The default is False.
76
- tolerance : float , optional
77
- The desired tolerance. The default is 0.0001.
78
-
79
- Returns
80
- -------
81
- topologic.Shell
82
- The created Shell.
83
-
84
- """
85
- from topologicpy.Vertex import Vertex
86
- from topologicpy.Edge import Edge
87
- from topologicpy.Wire import Wire
88
- from topologicpy.Face import Face
89
- from topologicpy.Cluster import Cluster
90
- from topologicpy.Helper import Helper
91
- from topologicpy.Topology import Topology
92
- from topologicpy.Grid import Grid
93
- from topologicpy.Dictionary import Dictionary
94
-
95
- def removeShards(edges, hostTopology, maximumGap=0.5):
96
- returnEdges = []
97
- for e in tqdm(edges, desc="Removing Shards", leave=False):
98
- if Edge.Length(e) < maximumGap:
99
- sv = Edge.StartVertex(e)
100
- ev = Edge.EndVertex(e)
101
- sEdges = Topology.SuperTopologies(sv, hostTopology, "edge")
102
- sn = len(sEdges)
103
- eEdges = Topology.SuperTopologies(ev, hostTopology, "edge")
104
- en = len(eEdges)
105
- if sn >= 2 and en >= 2:
106
- returnEdges.append(e)
107
- else:
108
- returnEdges.append(e)
109
- return returnEdges
110
-
111
- def extendEdges(edges, hostTopology, maximumGap=0.5):
112
- returnEdges = []
113
- for e in tqdm(edges, desc="Extending Edges", leave=False):
114
- sv = Edge.StartVertex(e)
115
- ev = Edge.EndVertex(e)
116
- sEdges = Topology.SuperTopologies(sv, hostTopology, "edge")
117
- sn = len(sEdges)
118
- eEdges = Topology.SuperTopologies(ev, hostTopology, "edge")
119
- en = len(eEdges)
120
- if sn == 1:
121
- ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=True)
122
- returnEdges.append(ee)
123
- elif en == 1:
124
- ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=False)
125
- returnEdges.append(ee)
126
- else:
127
- returnEdges.append(e)
128
- return returnEdges
129
-
130
- facesCluster = Cluster.ByTopologies(faces)
131
- internalBoundary = Face.ByWire(Face.InternalBoundaries(externalBoundary)[0], tolerance=tolerance)
132
- bb = Topology.BoundingBox(internalBoundary)
133
- bb_d = Topology.Dictionary(bb)
134
- unitU = Dictionary.ValueAtKey(bb_d, 'width') / uSides
135
- unitV = Dictionary.ValueAtKey(bb_d, 'length') / vSides
136
- uRange = [u*unitU for u in range(uSides)]
137
- vRange = [v*unitV for v in range(vSides)]
138
- grid = Grid.EdgesByDistances(internalBoundary, uRange=uRange, vRange=vRange, clip=True)
139
- grid = Topology.Slice(internalBoundary, grid, tolerance=tolerance)
140
- grid_faces = Topology.Faces(grid)
141
- skeletons = []
142
- for ib in tqdm(grid_faces, desc="Processing "+str(len(grid_faces))+" tiles", leave=False):
143
- building_shell = Topology.Slice(ib, facesCluster, tolerance=tolerance)
144
- wall_faces = Topology.Faces(building_shell)
145
- walls = []
146
- for w1 in wall_faces:
147
- iv = Topology.InternalVertex(w1, tolerance=tolerance)
148
- flag = False
149
- for w2 in faces:
150
- if Vertex.IsInternal(iv, w2):
151
- flag = True
152
- break;
153
- if flag == False:
154
- walls.append(w1)
155
- for wall in walls:
156
- skeleton = Wire.Skeleton(wall, tolerance=0.001) # This tolerance works better.
157
- skeleton = Topology.Difference(skeleton, facesCluster, tolerance=tolerance)
158
- skeleton = Topology.Difference(skeleton, Face.Wire(wall), tolerance=tolerance)
159
- skeletons.append(skeleton)
160
- if len(skeletons) > 0:
161
- skeleton_cluster = Cluster.ByTopologies(skeletons+[internalBoundary])
162
- skEdges = Topology.SelfMerge(Cluster.ByTopologies(removeShards(Topology.Edges(skeleton_cluster), skeleton_cluster, maximumGap=maximumGap)), tolerance=tolerance)
163
- if isinstance(skEdges, topologic.Edge):
164
- skEdges = extendEdges([skEdges], skEdges, maximumGap=maximumGap)
165
- else:
166
- skEdges = extendEdges(Topology.Edges(skEdges), skEdges, maximumGap=maximumGap)
167
- if len(skEdges) < 1:
168
- print("ShellByDisjointFaces - Warning: No edges were extended.")
169
- #return Cluster.ByTopologies(skEdges)
170
- #print("ShellByDisjointFaces - Error: Could not derive central skeleton of interior walls. Returning None.")
171
- #return None
172
-
173
- shell = Topology.Slice(internalBoundary, skeleton_cluster, tolerance=tolerance)
174
- if mergeJunctions == True:
175
- vertices = Shell.Vertices(shell)
176
- centers = []
177
- used = []
178
- for v in vertices:
179
- for w in vertices:
180
- if not Topology.IsSame(v, w) and not w in used:
181
- if Vertex.Distance(v, w) < threshold:
182
- centers.append(v)
183
- used.append(w)
184
- edges = Shell.Edges(shell)
185
- new_edges = []
186
- for e in edges:
187
- sv = Edge.StartVertex(e)
188
- ev = Edge.EndVertex(e)
189
- for v in centers:
190
- if Vertex.Distance(sv, v) < threshold:
191
- sv = v
192
- if Vertex.Distance(ev, v) < threshold:
193
- ev = v
194
- new_edges.append(Edge.ByVertices([sv,ev], tolerance=tolerance))
195
- cluster = Cluster.ByTopologies(new_edges)
196
-
197
- vertices = Topology.Vertices(cluster)
198
- edges = Topology.Edges(shell)
199
-
200
- xList = list(set([Vertex.X(v) for v in vertices]))
201
- xList.sort()
202
- xList = Helper.MergeByThreshold(xList, 0.5)
203
- yList = list(set([Vertex.Y(v) for v in vertices]))
204
- yList.sort()
205
- yList = Helper.MergeByThreshold(yList, 0.5)
206
- yList.sort()
207
-
208
- centers = []
209
-
210
- new_edges = []
211
-
212
- for e in edges:
213
- sv = Edge.StartVertex(e)
214
- ev = Edge.EndVertex(e)
215
- svx = Vertex.X(sv)
216
- svy = Vertex.Y(sv)
217
- evx = Vertex.X(ev)
218
- evy = Vertex.Y(ev)
219
- for x in xList:
220
- if abs(svx-x) < threshold:
221
- svx = x
222
- break;
223
- for y in yList:
224
- if abs(svy-y) < threshold:
225
- svy = y
226
- break;
227
- sv = Vertex.ByCoordinates(svx, svy, 0)
228
- for x in xList:
229
- if abs(evx-x) < threshold:
230
- evx = x
231
- break;
232
- for y in yList:
233
- if abs(evy-y) < threshold:
234
- evy = y
235
- break;
236
- sv = Vertex.ByCoordinates(svx, svy, 0)
237
- ev = Vertex.ByCoordinates(evx, evy, 0)
238
- new_edges.append(Edge.ByVertices([sv, ev], tolerance=tolerance))
239
-
240
- cluster = Cluster.ByTopologies(new_edges)
241
- eb = Face.ByWire(Shell.ExternalBoundary(shell), tolerance=tolerance)
242
- shell = Topology.Slice(eb, cluster, tolerance=tolerance)
243
- if not isinstance(shell, topologic.Shell):
244
- try:
245
- temp_wires = [Wire.RemoveCollinearEdges(w, angTolerance=1.0) for w in Topology.Wires(shell)]
246
- temp_faces = [Face.ByWire(w, tolerance=tolerance) for w in temp_wires]
247
- except:
248
- temp_faces = Topology.Faces(shell)
249
- shell = Shell.ByFaces(temp_faces, tolerance=tolerance)
250
- if transferDictionaries == True:
251
- selectors = []
252
- for f in faces:
253
- d = Topology.Dictionary(f)
254
- s = Topology.InternalVertex(f, tolerance=tolerance)
255
- s = Topology.SetDictionary(s, d)
256
- selectors.append(s)
257
- _ = Topology.TransferDictionariesBySelectors(topology=shell, selectors=selectors, tranFaces=True, tolerance=tolerance)
258
- return shell
259
- return None
260
-
261
- @staticmethod
262
- def ByFaces(faces: list, tolerance: float = 0.0001) -> topologic.Shell:
263
- """
264
- Creates a shell from the input list of faces.
265
-
266
- Parameters
267
- ----------
268
- faces : list
269
- The input list of faces.
270
- tolerance : float , optional
271
- The desired tolerance. The default is 0.0001.
272
-
273
- Returns
274
- -------
275
- topologic.Shell
276
- The created Shell.
277
-
278
- """
279
- from topologicpy.Topology import Topology
280
-
281
- if not isinstance(faces, list):
282
- return None
283
- faceList = [x for x in faces if isinstance(x, topologic.Face)]
284
- if len(faceList) == 0:
285
- print("Shell.ByFaces - Error: The input faces list does not contain any valid faces. Returning None.")
286
- return None
287
- shell = topologic.Shell.ByFaces(faceList, tolerance)
288
- if not isinstance(shell, topologic.Shell):
289
- shell = Topology.SelfMerge(shell, tolerance=tolerance)
290
- if isinstance(shell, topologic.Shell):
291
- return shell
292
- else:
293
- print("Shell.ByFaces - Error: Could not create shell. Returning None.")
294
- return None
295
- else:
296
- return shell
297
-
298
- @staticmethod
299
- def ByFacesCluster(cluster: topologic.Cluster, tolerance: float = 0.0001) -> topologic.Shell:
300
- """
301
- Creates a shell from the input cluster of faces.
302
-
303
- Parameters
304
- ----------
305
- cluster : topologic.Cluster
306
- The input cluster of faces.
307
- tolerance : float , optional
308
- The desired tolerance. The default is 0.0001.
309
-
310
- Returns
311
- -------
312
- topologic.Shell
313
- The created shell.
314
-
315
- """
316
- if not isinstance(cluster, topologic.Cluster):
317
- return None
318
- faces = []
319
- _ = cluster.Faces(None, faces)
320
- return Shell.ByFaces(faces, tolerance=tolerance)
321
-
322
- @staticmethod
323
- def ByWires(wires: list, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False) -> topologic.Shell:
324
- """
325
- Creates a shell by lofting through the input wires
326
- Parameters
327
- ----------
328
- wires : list
329
- The input list of wires.
330
- triangulate : bool , optional
331
- If set to True, the faces will be triangulated. The default is True.
332
- tolerance : float , optional
333
- The desired tolerance. The default is 0.0001.
334
- silent : bool , optional
335
- If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
336
-
337
- Returns
338
- -------
339
- topologic.Shell
340
- The creates shell.
341
- """
342
- from topologicpy.Edge import Edge
343
- from topologicpy.Wire import Wire
344
- from topologicpy.Face import Face
345
- if not isinstance(wires, list):
346
- return None
347
- wireList = [x for x in wires if isinstance(x, topologic.Wire)]
348
- faces = []
349
- for i in range(len(wireList)-1):
350
- wire1 = wireList[i]
351
- wire2 = wireList[i+1]
352
- if wire1.Type() < topologic.Edge.Type() or wire2.Type() < topologic.Edge.Type():
353
- return None
354
- if wire1.Type() == topologic.Edge.Type():
355
- w1_edges = [wire1]
356
- else:
357
- w1_edges = []
358
- _ = wire1.Edges(None, w1_edges)
359
- if wire2.Type() == topologic.Edge.Type():
360
- w2_edges = [wire2]
361
- else:
362
- w2_edges = []
363
- _ = wire2.Edges(None, w2_edges)
364
- if len(w1_edges) != len(w2_edges):
365
- return None
366
- if triangulate == True:
367
- for j in range (len(w1_edges)):
368
- e1 = w1_edges[j]
369
- e2 = w2_edges[j]
370
- e3 = None
371
- e4 = None
372
- try:
373
- e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
374
- except:
375
- e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
376
- faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e4], tolerance=tolerance), tolerance=tolerance))
377
- try:
378
- e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
379
- except:
380
- e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
381
- faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e3],tolerance=tolerance), tolerance=tolerance))
382
- if e3 and e4:
383
- e5 = Edge.ByVertices([e1.StartVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
384
- faces.append(Face.ByWire(Wire.ByEdges([e1, e5, e4], tolerance=tolerance), tolerance=tolerance))
385
- faces.append(Face.ByWire(Wire.ByEdges([e2, e5, e3], tolerance=tolerance), tolerance=tolerance))
386
- else:
387
- for j in range (len(w1_edges)):
388
- e1 = w1_edges[j]
389
- e2 = w2_edges[j]
390
- e3 = None
391
- e4 = None
392
- try:
393
- e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
394
- except:
395
- try:
396
- e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
397
- except:
398
- pass
399
- try:
400
- e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
401
- except:
402
- try:
403
- e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
404
- except:
405
- pass
406
- if e3 and e4:
407
- try:
408
- faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2, e3], tolerance=tolerance), tolerance=tolerance))
409
- except:
410
- faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2, e4], tolerance=tolerance), tolerance=tolerance))
411
- elif e3:
412
- faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2], tolerance=tolerance), tolerance=tolerance))
413
- elif e4:
414
- faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2], tolerance=tolerance), tolerance=tolerance))
415
- return Shell.ByFaces(faces, tolerance=tolerance)
416
-
417
- @staticmethod
418
- def ByWiresCluster(cluster: topologic.Cluster, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False) -> topologic.Shell:
419
- """
420
- Creates a shell by lofting through the input cluster of wires
421
-
422
- Parameters
423
- ----------
424
- wires : topologic.Cluster
425
- The input cluster of wires.
426
- triangulate : bool , optional
427
- If set to True, the faces will be triangulated. The default is True.
428
- tolerance : float , optional
429
- The desired tolerance. The default is 0.0001.
430
- silent : bool , optional
431
- If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
432
-
433
- Returns
434
- -------
435
- topologic.Shell
436
- The creates shell.
437
-
438
- """
439
- from topologicpy.Cluster import Cluster
440
- if not cluster:
441
- return None
442
- if not isinstance(cluster, topologic.Cluster):
443
- return None
444
- wires = Cluster.Wires(cluster)
445
- return Shell.ByWires(wires, triangulate=triangulate, tolerance=tolerance, silent=silent)
446
-
447
- @staticmethod
448
- def Circle(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
449
- """
450
- Creates a circle.
451
-
452
- Parameters
453
- ----------
454
- origin : topologic.Vertex , optional
455
- The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
456
- radius : float , optional
457
- The radius of the circle. The default is 0.5.
458
- sides : int , optional
459
- The number of sides of the circle. The default is 32.
460
- fromAngle : float , optional
461
- The angle in degrees from which to start creating the arc of the circle. The default is 0.
462
- toAngle : float , optional
463
- The angle in degrees at which to end creating the arc of the circle. The default is 360.
464
- direction : list , optional
465
- The vector representing the up direction of the circle. The default is [0, 0, 1].
466
- placement : str , optional
467
- The description of the placement of the origin of the pie. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
468
- tolerance : float , optional
469
- The desired tolerance. The default is 0.0001.
470
-
471
- Returns
472
- -------
473
- topologic.Shell
474
- The created circle.
475
- """
476
- return Shell.Pie(origin=origin, radiusA=radius, radiusB=0, sides=sides, rings=1, fromAngle=fromAngle, toAngle=toAngle, direction=direction, placement=placement, tolerance=tolerance)
477
-
478
- @staticmethod
479
- def Delaunay(vertices: list, face: topologic.Face = None, tolerance: float = 0.0001) -> topologic.Shell:
480
- """
481
- Returns a delaunay partitioning of the input vertices. The vertices must be coplanar. See https://en.wikipedia.org/wiki/Delaunay_triangulation.
482
-
483
- Parameters
484
- ----------
485
- vertices : list
486
- The input list of vertices.
487
- face : topologic.Face , optional
488
- The input face. If specified, the delaunay triangulation is clipped to the face.
489
- tolerance : float , optional
490
- The desired tolerance. The default is 0.0001.
491
-
492
- Returns
493
- -------
494
- shell
495
- A shell representing the delaunay triangulation of the input vertices.
496
-
497
- """
498
- from topologicpy.Vertex import Vertex
499
- from topologicpy.Wire import Wire
500
- from topologicpy.Face import Face
501
- from topologicpy.Cluster import Cluster
502
- from topologicpy.Topology import Topology
503
- from topologicpy.Dictionary import Dictionary
504
- from random import sample
505
- from scipy.spatial import Delaunay as SCIDelaunay
506
- import numpy as np
507
-
508
- if not isinstance(vertices, list):
509
- return None
510
- vertices = [x for x in vertices if isinstance(x, topologic.Vertex)]
511
- if len(vertices) < 3:
512
- return None
513
-
514
- # Create a Vertex at the world's origin (0, 0, 0)
515
- world_origin = Vertex.Origin()
516
-
517
- if isinstance(face, topologic.Face):
518
- # Flatten the face
519
- origin = Topology.Centroid(face)
520
- normal = Face.Normal(face)
521
- flatFace = Topology.Flatten(face, origin=origin, direction=normal)
522
- faceVertices = Face.Vertices(face)
523
- vertices += faceVertices
524
-
525
- # Create a cluster of the input vertices
526
- verticesCluster = Cluster.ByTopologies(vertices)
527
-
528
- # Flatten the cluster using the same transformations
529
- verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal)
530
-
531
- vertices = Cluster.Vertices(verticesCluster)
532
- points = []
533
- for v in vertices:
534
- points.append([Vertex.X(v), Vertex.Y(v)])
535
- delaunay = SCIDelaunay(points)
536
- simplices = delaunay.simplices
537
-
538
- faces = []
539
- for simplex in simplices:
540
- tempTriangleVertices = []
541
- tempTriangleVertices.append(vertices[simplex[0]])
542
- tempTriangleVertices.append(vertices[simplex[1]])
543
- tempTriangleVertices.append(vertices[simplex[2]])
544
- tempFace = Face.ByWire(Wire.ByVertices(tempTriangleVertices), tolerance=tolerance)
545
- faces.append(tempFace)
546
-
547
- shell = Shell.ByFaces(faces, tolerance=tolerance)
548
- if shell == None:
549
- shell = Cluster.ByTopologies(faces)
550
-
551
- if isinstance(face, topologic.Face):
552
- edges = Topology.Edges(shell)
553
- shell = Topology.Slice(flatFace, Cluster.ByTopologies(edges))
554
- # Get the internal boundaries of the face
555
- wires = Face.InternalBoundaries(flatFace)
556
- ibList = []
557
- if len(wires) > 0:
558
- ibList = [Face.ByWire(w) for w in wires]
559
- cluster = Cluster.ByTopologies(ibList)
560
- shell = Topology.Difference(shell, cluster)
561
- shell = Topology.Unflatten(shell, origin=origin, direction=normal)
562
- return shell
563
-
564
- @staticmethod
565
- def Edges(shell: topologic.Shell) -> list:
566
- """
567
- Returns the edges of the input shell.
568
-
569
- Parameters
570
- ----------
571
- shell : topologic.Shell
572
- The input shell.
573
-
574
- Returns
575
- -------
576
- list
577
- The list of edges.
578
-
579
- """
580
- if not isinstance(shell, topologic.Shell):
581
- return None
582
- edges = []
583
- _ = shell.Edges(None, edges)
584
- return edges
585
-
586
- @staticmethod
587
- def ExternalBoundary(shell: topologic.Shell, tolerance: float = 0.0001) -> topologic.Wire:
588
- """
589
- Returns the external boundary of the input shell.
590
-
591
- Parameters
592
- ----------
593
- shell : topologic.Shell
594
- The input shell.
595
- tolerance : float , optional
596
- The desired tolerance. The default is 0.0001.
597
-
598
- Returns
599
- -------
600
- topologic.Wire or topologic.Cluster
601
- The external boundary of the input shell. If the shell has holes, the return value will be a cluster of wires.
602
-
603
- """
604
- from topologicpy.Wire import Wire
605
- from topologicpy.Cluster import Cluster
606
- from topologicpy.Topology import Topology
607
-
608
- if not isinstance(shell, topologic.Shell):
609
- return None
610
- edges = []
611
- _ = shell.Edges(None, edges)
612
- obEdges = []
613
- for anEdge in edges:
614
- faces = []
615
- _ = anEdge.Faces(shell, faces)
616
- if len(faces) == 1:
617
- obEdges.append(anEdge)
618
- return Topology.SelfMerge(Cluster.ByTopologies(obEdges), tolerance=tolerance)
619
-
620
- @staticmethod
621
- def Faces(shell: topologic.Shell) -> list:
622
- """
623
- Returns the faces of the input shell.
624
-
625
- Parameters
626
- ----------
627
- shell : topologic.Shell
628
- The input shell.
629
-
630
- Returns
631
- -------
632
- list
633
- The list of faces.
634
-
635
- """
636
- if not isinstance(shell, topologic.Shell):
637
- return None
638
- faces = []
639
- _ = shell.Faces(None, faces)
640
- return faces
641
-
642
- @staticmethod
643
- def IsOnBoundary(shell: topologic.Shell, vertex: topologic.Vertex, tolerance: float = 0.0001) -> bool:
644
- """
645
- Returns True if the input vertex is on the boundary of the input shell. Returns False otherwise. On the boundary is defined as being on the boundary of one of the shell's external or internal boundaries
646
-
647
- Parameters
648
- ----------
649
- shell : topologic.Shell
650
- The input shell.
651
- vertex : topologic.Vertex
652
- The input vertex.
653
- tolerance : float , optional
654
- The desired tolerance. The default is 0.0001.
655
-
656
- Returns
657
- -------
658
- bool
659
- Returns True if the input vertex is inside the input shell. Returns False otherwise.
660
-
661
- """
662
-
663
- from topologicpy.Vertex import Vertex
664
-
665
- if not isinstance(shell, topologic.Shell):
666
- return None
667
- if not isinstance(vertex, topologic.Vertex):
668
- return None
669
- boundary = Shell.ExternalBoundary(shell, tolerance=tolerance)
670
- if Vertex.IsInternal(vertex, boundary, tolerance=tolerance):
671
- return True
672
- internal_boundaries = Shell.InternalBoundaries(shell, tolerance=tolerance)
673
- for ib in internal_boundaries:
674
- if Vertex.IsInternal(vertex, ib, tolerance=tolerance):
675
- return True
676
- return False
677
-
678
- @staticmethod
679
- def HyperbolicParaboloidRectangularDomain(origin: topologic.Vertex = None, llVertex: topologic.Vertex = None, lrVertex: topologic.Vertex =None, ulVertex: topologic.Vertex =None, urVertex: topologic.Vertex = None,
680
- uSides: int = 10, vSides: int = 10, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
681
- """
682
- Creates a hyperbolic paraboloid with a rectangular domain.
683
-
684
- Parameters
685
- ----------
686
- origin : topologic.Vertex , optional
687
- The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
688
- llVertex : topologic.Vertex , optional
689
- The lower left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, -0.5, -0.5).
690
- lrVertex : topologic.Vertex , optional
691
- The lower right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, -0.5, 0.5).
692
- ulVertex : topologic.Vertex , optional
693
- The upper left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, 0.5, 0.5).
694
- urVertex : topologic.Vertex , optional
695
- The upper right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, 0.5, -0.5).
696
- uSides : int , optional
697
- The number of segments along the X axis. The default is 10.
698
- vSides : int , optional
699
- The number of segments along the Y axis. The default is 10.
700
- direction : list , optional
701
- The vector representing the up direction of the hyperbolic parabolid. The default is [0, 0, 1].
702
- placement : str , optional
703
- The description of the placement of the origin of the hyperbolic parabolid. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center".
704
- tolerance : float , optional
705
- The desired tolerance. The default is 0.0001.
706
- Returns
707
- -------
708
- topologic.Shell
709
- The created hyperbolic paraboloid.
710
-
711
- """
712
- from topologicpy.Vertex import Vertex
713
- from topologicpy.Edge import Edge
714
- from topologicpy.Face import Face
715
- from topologicpy.Topology import Topology
716
- if not isinstance(origin, topologic.Vertex):
717
- origin = Vertex.ByCoordinates(0, 0, 0)
718
- if not isinstance(llVertex, topologic.Vertex):
719
- llVertex = Vertex.ByCoordinates(-0.5, -0.5, -0.5)
720
- if not isinstance(lrVertex, topologic.Vertex):
721
- lrVertex = Vertex.ByCoordinates(0.5, -0.5, 0.5)
722
- if not isinstance(ulVertex, topologic.Vertex):
723
- ulVertex = Vertex.ByCoordinates(-0.5, 0.5, 0.5)
724
- if not isinstance(urVertex, topologic.Vertex):
725
- urVertex = Vertex.ByCoordinates(0.5, 0.5, -0.5)
726
- e1 = Edge.ByVertices([llVertex, lrVertex], tolerance=tolerance)
727
- e3 = Edge.ByVertices([urVertex, ulVertex], tolerance=tolerance)
728
- edges = []
729
- for i in range(uSides+1):
730
- v1 = Edge.VertexByParameter(e1, float(i)/float(uSides))
731
- v2 = Edge.VertexByParameter(e3, 1.0 - float(i)/float(uSides))
732
- edges.append(Edge.ByVertices([v1, v2], tolerance=tolerance))
733
- faces = []
734
- for i in range(uSides):
735
- for j in range(vSides):
736
- v1 = Edge.VertexByParameter(edges[i], float(j)/float(vSides))
737
- v2 = Edge.VertexByParameter(edges[i], float(j+1)/float(vSides))
738
- v3 = Edge.VertexByParameter(edges[i+1], float(j+1)/float(vSides))
739
- v4 = Edge.VertexByParameter(edges[i+1], float(j)/float(vSides))
740
- faces.append(Face.ByVertices([v1, v2, v4]))
741
- faces.append(Face.ByVertices([v4, v2, v3]))
742
- returnTopology = Shell.ByFaces(faces, tolerance=tolerance)
743
- if not returnTopology:
744
- returnTopology = None
745
- xOffset = 0
746
- yOffset = 0
747
- zOffset = 0
748
- minX = min([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()])
749
- maxX = max([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()])
750
- minY = min([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()])
751
- maxY = max([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()])
752
- minZ = min([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()])
753
- maxZ = max([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()])
754
- if placement.lower() == "lowerleft":
755
- xOffset = -minX
756
- yOffset = -minY
757
- zOffset = -minZ
758
- elif placement.lower() == "bottom":
759
- xOffset = -(minX + (maxX - minX)*0.5)
760
- yOffset = -(minY + (maxY - minY)*0.5)
761
- zOffset = -minZ
762
- elif placement.lower() == "center":
763
- xOffset = -(minX + (maxX - minX)*0.5)
764
- yOffset = -(minY + (maxY - minY)*0.5)
765
- zOffset = -(minZ + (maxZ - minZ)*0.5)
766
- returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset)
767
- returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin)
768
- returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction)
769
- return returnTopology
770
-
771
- @staticmethod
772
- def HyperbolicParaboloidCircularDomain(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 36, rings: int = 10,
773
- A: float = 2.0, B: float = -2.0, direction: list = [0, 0, 1],
774
- placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
775
- """
776
- Creates a hyperbolic paraboloid with a circular domain. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
777
-
778
- Parameters
779
- ----------
780
- origin : topologic.Vertex , optional
781
- The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
782
- radius : float , optional
783
- The desired radius of the hyperbolic paraboloid. The default is 0.5.
784
- sides : int , optional
785
- The desired number of sides of the hyperbolic parabolid. The default is 36.
786
- rings : int , optional
787
- The desired number of concentric rings of the hyperbolic parabolid. The default is 10.
788
- A : float , optional
789
- The *A* constant in the equation z = A*x^2^ + B*y^2^. The default is 2.0.
790
- B : float , optional
791
- The *B* constant in the equation z = A*x^2^ + B*y^2^. The default is -2.0.
792
- direction : list , optional
793
- The vector representing the up direction of the hyperbolic paraboloid. The default is [0, 0, 1].
794
- placement : str , optional
795
- The description of the placement of the origin of the circle. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center".
796
- tolerance : float , optional
797
- The desired tolerance. The default is 0.0001.
798
- Returns
799
- -------
800
- topologic.Shell
801
- The created hyperboloic paraboloid.
802
-
803
- """
804
- from topologicpy.Vertex import Vertex
805
- from topologicpy.Face import Face
806
- from topologicpy.Topology import Topology
807
- if not isinstance(origin, topologic.Vertex):
808
- origin = Vertex.ByCoordinates(0, 0, 0)
809
- uOffset = float(360)/float(sides)
810
- vOffset = float(radius)/float(rings)
811
- faces = []
812
- for i in range(rings-1):
813
- r1 = radius - vOffset*i
814
- r2 = radius - vOffset*(i+1)
815
- for j in range(sides-1):
816
- a1 = math.radians(uOffset)*j
817
- a2 = math.radians(uOffset)*(j+1)
818
- x1 = math.sin(a1)*r1
819
- y1 = math.cos(a1)*r1
820
- z1 = A*x1*x1 + B*y1*y1
821
- x2 = math.sin(a1)*r2
822
- y2 = math.cos(a1)*r2
823
- z2 = A*x2*x2 + B*y2*y2
824
- x3 = math.sin(a2)*r2
825
- y3 = math.cos(a2)*r2
826
- z3 = A*x3*x3 + B*y3*y3
827
- x4 = math.sin(a2)*r1
828
- y4 = math.cos(a2)*r1
829
- z4 = A*x4*x4 + B*y4*y4
830
- v1 = topologic.Vertex.ByCoordinates(x1,y1,z1)
831
- v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
832
- v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
833
- v4 = topologic.Vertex.ByCoordinates(x4,y4,z4)
834
- f1 = Face.ByVertices([v1,v2,v4])
835
- f2 = Face.ByVertices([v4,v2,v3])
836
- faces.append(f1)
837
- faces.append(f2)
838
- a1 = math.radians(uOffset)*(sides-1)
839
- a2 = math.radians(360)
840
- x1 = math.sin(a1)*r1
841
- y1 = math.cos(a1)*r1
842
- z1 = A*x1*x1 + B*y1*y1
843
- x2 = math.sin(a1)*r2
844
- y2 = math.cos(a1)*r2
845
- z2 = A*x2*x2 + B*y2*y2
846
- x3 = math.sin(a2)*r2
847
- y3 = math.cos(a2)*r2
848
- z3 = A*x3*x3 + B*y3*y3
849
- x4 = math.sin(a2)*r1
850
- y4 = math.cos(a2)*r1
851
- z4 = A*x4*x4 + B*y4*y4
852
- v1 = topologic.Vertex.ByCoordinates(x1,y1,z1)
853
- v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
854
- v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
855
- v4 = topologic.Vertex.ByCoordinates(x4,y4,z4)
856
- f1 = Face.ByVertices([v1,v2,v4])
857
- f2 = Face.ByVertices([v4,v2,v3])
858
- faces.append(f1)
859
- faces.append(f2)
860
- # Special Case: Center triangles
861
- r = vOffset
862
- x1 = 0
863
- y1 = 0
864
- z1 = 0
865
- v1 = topologic.Vertex.ByCoordinates(x1,y1,z1)
866
- for j in range(sides-1):
867
- a1 = math.radians(uOffset)*j
868
- a2 = math.radians(uOffset)*(j+1)
869
- x2 = math.sin(a1)*r
870
- y2 = math.cos(a1)*r
871
- z2 = A*x2*x2 + B*y2*y2
872
- #z2 = 0
873
- x3 = math.sin(a2)*r
874
- y3 = math.cos(a2)*r
875
- z3 = A*x3*x3 + B*y3*y3
876
- #z3 = 0
877
- v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
878
- v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
879
- f1 = Face.ByVertices([v2,v1,v3])
880
- faces.append(f1)
881
- a1 = math.radians(uOffset)*(sides-1)
882
- a2 = math.radians(360)
883
- x2 = math.sin(a1)*r
884
- y2 = math.cos(a1)*r
885
- z2 = A*x2*x2 + B*y2*y2
886
- x3 = math.sin(a2)*r
887
- y3 = math.cos(a2)*r
888
- z3 = A*x3*x3 + B*y3*y3
889
- v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
890
- v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
891
- f1 = Face.ByVertices([v2,v1,v3])
892
- faces.append(f1)
893
- returnTopology = topologic.Shell.ByFaces(faces, tolerance)
894
- if not returnTopology:
895
- returnTopology = topologic.Cluster.ByTopologies(faces)
896
- vertices = []
897
- _ = returnTopology.Vertices(None, vertices)
898
- xList = []
899
- yList = []
900
- zList = []
901
- for aVertex in vertices:
902
- xList.append(aVertex.X())
903
- yList.append(aVertex.Y())
904
- zList.append(aVertex.Z())
905
- minX = min(xList)
906
- maxX = max(xList)
907
- minY = min(yList)
908
- maxY = max(yList)
909
- minZ = min(zList)
910
- maxZ = max(zList)
911
- xOffset = 0
912
- yOffset = 0
913
- zOffset = 0
914
- if placement.lower() == "lowerleft":
915
- xOffset = -minX
916
- yOffset = -minY
917
- zOffset = -minZ
918
- elif placement.lower() == "bottom":
919
- xOffset = -(minX + (maxX - minX)*0.5)
920
- yOffset = -(minY + (maxY - minY)*0.5)
921
- zOffset = -minZ
922
- elif placement.lower() == "center":
923
- xOffset = -(minX + (maxX - minX)*0.5)
924
- yOffset = -(minY + (maxY - minY)*0.5)
925
- zOffset = -(minZ + (maxZ - minZ)*0.5)
926
- returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset)
927
- returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin)
928
- returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction)
929
- return returnTopology
930
-
931
- @staticmethod
932
- def InternalBoundaries(shell: topologic.Shell, tolerance=0.0001) -> topologic.Topology:
933
- """
934
- Returns the internal boundaries (closed wires) of the input shell. Internal boundaries are considered holes.
935
-
936
- Parameters
937
- ----------
938
- shell : topologic.Shell
939
- The input shell.
940
- tolerance : float , optional
941
- The desired tolerance. The default is 0.0001.
942
-
943
- Returns
944
- -------
945
- list
946
- The list of internal boundaries
947
-
948
- """
949
- from topologicpy.Cluster import Cluster
950
- from topologicpy.Topology import Topology
951
- edges = []
952
- _ = shell.Edges(None, edges)
953
- ibEdges = []
954
- for anEdge in edges:
955
- faces = []
956
- _ = anEdge.Faces(shell, faces)
957
- if len(faces) > 1:
958
- ibEdges.append(anEdge)
959
- returnTopology = Topology.SelfMerge(Cluster.ByTopologies(ibEdges), tolerance=tolerance)
960
- wires = Topology.Wires(returnTopology)
961
- return wires
962
-
963
-
964
- @staticmethod
965
- def IsClosed(shell: topologic.Shell) -> bool:
966
- """
967
- Returns True if the input shell is closed. Returns False otherwise.
968
-
969
- Parameters
970
- ----------
971
- shell : topologic.Shell
972
- The input shell.
973
-
974
- Returns
975
- -------
976
- bool
977
- True if the input shell is closed. False otherwise.
978
-
979
- """
980
- return shell.IsClosed()
981
-
982
- @staticmethod
983
- def Pie(origin: topologic.Vertex = None, radiusA: float = 0.5, radiusB: float = 0.0, sides: int = 32, rings: int = 1, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
984
- """
985
- Creates a pie shape.
986
-
987
- Parameters
988
- ----------
989
- origin : topologic.Vertex , optional
990
- The location of the origin of the pie. The default is None which results in the pie being placed at (0, 0, 0).
991
- radiusA : float , optional
992
- The outer radius of the pie. The default is 0.5.
993
- radiusB : float , optional
994
- The inner radius of the pie. The default is 0.25.
995
- sides : int , optional
996
- The number of sides of the pie. The default is 32.
997
- rings : int , optional
998
- The number of rings of the pie. The default is 1.
999
- fromAngle : float , optional
1000
- The angle in degrees from which to start creating the arc of the pie. The default is 0.
1001
- toAngle : float , optional
1002
- The angle in degrees at which to end creating the arc of the pie. The default is 360.
1003
- direction : list , optional
1004
- The vector representing the up direction of the pie. The default is [0, 0, 1].
1005
- placement : str , optional
1006
- The description of the placement of the origin of the pie. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1007
- tolerance : float , optional
1008
- The desired tolerance. The default is 0.0001.
1009
-
1010
- Returns
1011
- -------
1012
- topologic.Shell
1013
- The created pie.
1014
-
1015
- """
1016
- from topologicpy.Vertex import Vertex
1017
- from topologicpy.Face import Face
1018
- from topologicpy.Topology import Topology
1019
- if not origin:
1020
- origin = Vertex.ByCoordinates(0, 0, 0)
1021
- if not isinstance(origin, topologic.Vertex):
1022
- return None
1023
- if toAngle < fromAngle:
1024
- toAngle += 360
1025
- if abs(toAngle-fromAngle) < tolerance:
1026
- return None
1027
- fromAngle = math.radians(fromAngle)
1028
- toAngle = math.radians(toAngle)
1029
- angleRange = toAngle - fromAngle
1030
- radiusA = abs(radiusA)
1031
- radiusB = abs(radiusB)
1032
- if radiusB > radiusA:
1033
- temp = radiusA
1034
- radiusA = radiusB
1035
- radiusB = temp
1036
- if abs(radiusA - radiusB) < tolerance or radiusA < tolerance:
1037
- return None
1038
- radiusRange = radiusA - radiusB
1039
- sides = int(abs(math.floor(sides)))
1040
- if sides < 3:
1041
- return None
1042
- rings = int(abs(rings))
1043
- if radiusB < tolerance:
1044
- radiusB = 0
1045
- xOffset = 0
1046
- yOffset = 0
1047
- zOffset = 0
1048
- if placement.lower() == "lowerleft":
1049
- xOffset = radiusA
1050
- yOffset = radiusA
1051
- uOffset = float(angleRange)/float(sides)
1052
- vOffset = float(radiusRange)/float(rings)
1053
- faces = []
1054
- if radiusB > tolerance:
1055
- for i in range(rings):
1056
- r1 = radiusA - vOffset*i
1057
- r2 = radiusA - vOffset*(i+1)
1058
- for j in range(sides):
1059
- a1 = fromAngle + uOffset*j
1060
- a2 = fromAngle + uOffset*(j+1)
1061
- x1 = math.sin(a1)*r1
1062
- y1 = math.cos(a1)*r1
1063
- z1 = 0
1064
- x2 = math.sin(a1)*r2
1065
- y2 = math.cos(a1)*r2
1066
- z2 = 0
1067
- x3 = math.sin(a2)*r2
1068
- y3 = math.cos(a2)*r2
1069
- z3 = 0
1070
- x4 = math.sin(a2)*r1
1071
- y4 = math.cos(a2)*r1
1072
- z4 = 0
1073
- v1 = Vertex.ByCoordinates(x1,y1,z1)
1074
- v2 = Vertex.ByCoordinates(x2,y2,z2)
1075
- v3 = Vertex.ByCoordinates(x3,y3,z3)
1076
- v4 = Vertex.ByCoordinates(x4,y4,z4)
1077
- f1 = Face.ByVertices([v1,v2,v3,v4])
1078
- faces.append(f1)
1079
- else:
1080
- x1 = 0
1081
- y1 = 0
1082
- z1 = 0
1083
- v1 = Vertex.ByCoordinates(x1,y1,z1)
1084
- for j in range(sides):
1085
- a1 = fromAngle + uOffset*j
1086
- a2 = fromAngle + uOffset*(j+1)
1087
- x2 = math.sin(a1)*radiusA
1088
- y2 = math.cos(a1)*radiusA
1089
- z2 = 0
1090
- x3 = math.sin(a2)*radiusA
1091
- y3 = math.cos(a2)*radiusA
1092
- z3 = 0
1093
- v2 = Vertex.ByCoordinates(x2,y2,z2)
1094
- v3 = Vertex.ByCoordinates(x3,y3,z3)
1095
- f1 = Face.ByVertices([v2,v1,v3])
1096
- faces.append(f1)
1097
-
1098
- shell = Shell.ByFaces(faces, tolerance=tolerance)
1099
- if not shell:
1100
- return None
1101
- shell = Topology.Translate(shell, xOffset, yOffset, zOffset)
1102
- shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin)
1103
- shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction)
1104
- return shell
1105
-
1106
-
1107
- @staticmethod
1108
- def Planarize(shell: topologic.Shell, origin: topologic.Vertex = None, mantissa: int = 6, tolerance: float = 0.0001) -> topologic.Shell:
1109
- """
1110
- Returns a planarized version of the input shell.
1111
-
1112
- Parameters
1113
- ----------
1114
- shell : topologic.Shell
1115
- The input shell.
1116
- tolerance : float, optional
1117
- The desired tolerance. The default is 0.0001.
1118
- origin : topologic.Vertex , optional
1119
- The desired origin of the plane unto which the planar shell will be projected. If set to None, the centroid of the input shell will be chosen. The default is None.
1120
- mantissa : int , optional
1121
- The desired length of the mantissa. The default is 6.
1122
-
1123
- Returns
1124
- -------
1125
- topologic.Shell
1126
- The planarized shell.
1127
-
1128
- """
1129
- from topologicpy.Vertex import Vertex
1130
- from topologicpy.Face import Face
1131
- from topologicpy.Cluster import Cluster
1132
- from topologicpy.Topology import Topology
1133
-
1134
- if not isinstance(shell, topologic.Shell):
1135
- print("Shell.Planarize - Error: The input wire parameter is not a valid topologic shell. Returning None.")
1136
- return None
1137
- if origin == None:
1138
- origin = Vertex.Origin()
1139
- if not isinstance(origin, topologic.Vertex):
1140
- print("Shell.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
1141
- return None
1142
-
1143
- vertices = Topology.Vertices(shell)
1144
- faces = Topology.Faces(shell)
1145
- plane_equation = Vertex.PlaneEquation(vertices, mantissa=mantissa)
1146
- rect = Face.RectangleByPlaneEquation(origin=origin , equation=plane_equation, tolerance=tolerance)
1147
- new_vertices = [Vertex.Project(v, rect, mantissa=mantissa) for v in vertices]
1148
- new_shell = Topology.ReplaceVertices(shell, verticesA=vertices, verticesB=new_vertices)
1149
- new_faces = Topology.Faces(new_shell)
1150
- return Topology.SelfMerge(Cluster.ByTopologies(new_faces), tolerance=tolerance)
1151
-
1152
- @staticmethod
1153
- def Rectangle(origin: topologic.Vertex = None, width: float = 1.0, length: float = 1.0,
1154
- uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1],
1155
- placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
1156
- """
1157
- Creates a rectangle.
1158
-
1159
- Parameters
1160
- ----------
1161
- origin : topologic.Vertex , optional
1162
- The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0).
1163
- width : float , optional
1164
- The width of the rectangle. The default is 1.0.
1165
- length : float , optional
1166
- The length of the rectangle. The default is 1.0.
1167
- uSides : int , optional
1168
- The number of sides along the width. The default is 2.
1169
- vSides : int , optional
1170
- The number of sides along the length. The default is 2.
1171
- direction : list , optional
1172
- The vector representing the up direction of the rectangle. The default is [0, 0, 1].
1173
- placement : str , optional
1174
- The description of the placement of the origin of the rectangle. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1175
- tolerance : float , optional
1176
- The desired tolerance. The default is 0.0001.
1177
-
1178
- Returns
1179
- -------
1180
- topologic.Shell
1181
- The created shell.
1182
-
1183
- """
1184
- from topologicpy.Vertex import Vertex
1185
- from topologicpy.Wire import Wire
1186
- from topologicpy.Face import Face
1187
- if not origin:
1188
- origin = Vertex.ByCoordinates(0, 0, 0)
1189
- if not isinstance(origin, topologic.Vertex):
1190
- return None
1191
- uOffset = float(width)/float(uSides)
1192
- vOffset = float(length)/float(vSides)
1193
- faces = []
1194
- if placement.lower() == "center":
1195
- wOffset = width*0.5
1196
- lOffset = length*0.5
1197
- else:
1198
- wOffset = 0
1199
- lOffset = 0
1200
- for i in range(uSides):
1201
- for j in range(vSides):
1202
- rOrigin = Vertex.ByCoordinates(i*uOffset - wOffset, j*vOffset - lOffset, 0)
1203
- w = Wire.Rectangle(origin=rOrigin, width=uOffset, length=vOffset, direction=[0, 0, 1], placement="lowerleft", tolerance=tolerance)
1204
- f = Face.ByWire(w, tolerance=tolerance)
1205
- faces.append(f)
1206
- shell = Shell.ByFaces(faces, tolerance=tolerance)
1207
- shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin)
1208
- shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction)
1209
- return shell
1210
-
1211
- @staticmethod
1212
- def RemoveCollinearEdges(shell: topologic.Shell, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Wire:
1213
- """
1214
- Removes any collinear edges in the input shell.
1215
-
1216
- Parameters
1217
- ----------
1218
- shell : topologic.Shell
1219
- The input shell.
1220
- angTolerance : float , optional
1221
- The desired angular tolerance. The default is 0.1.
1222
- tolerance : float , optional
1223
- The desired tolerance. The default is 0.0001.
1224
-
1225
- Returns
1226
- -------
1227
- topologic.Shell
1228
- The created shell without any collinear edges.
1229
-
1230
- """
1231
- from topologicpy.Face import Face
1232
-
1233
- if not isinstance(shell, topologic.Shell):
1234
- print("Shell.RemoveCollinearEdges - Error: The input shell parameter is not a valid shell. Returning None.")
1235
- return None
1236
- faces = Shell.Faces(shell)
1237
- clean_faces = []
1238
- for face in faces:
1239
- clean_faces.append(Face.RemoveCollinearEdges(face, angTolerance=angTolerance, tolerance=tolerance))
1240
- return Shell.ByFaces(clean_faces, tolerance=tolerance)
1241
-
1242
- @staticmethod
1243
- def Roof(face, angle: float = 45, epsilon: float = 0.01, tolerance: float = 0.001):
1244
- """
1245
- Creates a hipped roof through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
1246
- This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
1247
-
1248
- Parameters
1249
- ----------
1250
- face : topologic.Face
1251
- The input face.
1252
- angle : float , optioal
1253
- The desired angle in degrees of the roof. The default is 45.
1254
- epsilon : float , optional
1255
- The desired epsilon (another form of tolerance for distance from plane). The default is 0.01. (This is set to a larger number as it was found to work better)
1256
- tolerance : float , optional
1257
- The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
1258
-
1259
- Returns
1260
- -------
1261
- topologic.Shell
1262
- The created roof.
1263
-
1264
- """
1265
- from topologicpy.Vertex import Vertex
1266
- from topologicpy.Wire import Wire
1267
- from topologicpy.Face import Face
1268
- from topologicpy.Shell import Shell
1269
- from topologicpy.Cell import Cell
1270
- from topologicpy.Cluster import Cluster
1271
- from topologicpy.Topology import Topology
1272
- from topologicpy.Dictionary import Dictionary
1273
- import topologic
1274
- import math
1275
-
1276
- def nearest_vertex_2d(v, vertices, tolerance=0.001):
1277
- for vertex in vertices:
1278
- x2 = Vertex.X(vertex)
1279
- y2 = Vertex.Y(vertex)
1280
- temp_v = Vertex.ByCoordinates(x2, y2, Vertex.Z(v))
1281
- if Vertex.Distance(v, temp_v) <= tolerance:
1282
- return vertex
1283
- return None
1284
-
1285
- if not isinstance(face, topologic.Face):
1286
- return None
1287
- angle = abs(angle)
1288
- if angle >= 90-tolerance:
1289
- return None
1290
- if angle < tolerance:
1291
- return None
1292
- origin = Topology.Centroid(face)
1293
- normal = Face.Normal(face)
1294
- flat_face = Topology.Flatten(face, origin=origin, direction=normal)
1295
- roof = Wire.Roof(flat_face, angle=angle, tolerance=tolerance)
1296
- if not roof:
1297
- return None
1298
- shell = Shell.Skeleton(flat_face, tolerance=tolerance)
1299
- faces = Shell.Faces(shell)
1300
- Topology.Show(shell)
1301
- if not faces:
1302
- return None
1303
- triangles = []
1304
- for face in faces:
1305
- internalBoundaries = Face.InternalBoundaries(face)
1306
- if len(internalBoundaries) == 0:
1307
- if len(Topology.Vertices(face)) > 3:
1308
- triangles += Face.Triangulate(face, tolerance=tolerance)
1309
- else:
1310
- triangles += [face]
1311
-
1312
- roof_vertices = Topology.Vertices(roof)
1313
- flat_vertices = []
1314
- for rv in roof_vertices:
1315
- flat_vertices.append(Vertex.ByCoordinates(Vertex.X(rv), Vertex.Y(rv), 0))
1316
-
1317
- final_triangles = []
1318
- for triangle in triangles:
1319
- if len(Topology.Vertices(triangle)) > 3:
1320
- triangles = Face.Triangulate(triangle, tolerance=tolerance)
1321
- else:
1322
- triangles = [triangle]
1323
- final_triangles += triangles
1324
-
1325
- final_faces = []
1326
- for triangle in final_triangles:
1327
- face_vertices = Topology.Vertices(triangle)
1328
- top_vertices = []
1329
- for sv in face_vertices:
1330
- temp = nearest_vertex_2d(sv, roof_vertices, tolerance=tolerance)
1331
- if temp:
1332
- top_vertices.append(temp)
1333
- else:
1334
- top_vertices.append(sv)
1335
- tri_face = Face.ByVertices(top_vertices)
1336
- final_faces.append(tri_face)
1337
-
1338
- shell = Shell.ByFaces(final_faces, tolerance=tolerance)
1339
- if not shell:
1340
- shell = Cluster.ByTopologies(final_faces)
1341
- try:
1342
- shell = Topology.RemoveCoplanarFaces(shell, epsilon=epsilon, tolerance=tolerance)
1343
- except:
1344
- pass
1345
- return shell
1346
-
1347
- @staticmethod
1348
- def SelfMerge(shell: topologic.Shell, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Face:
1349
- """
1350
- Creates a face by merging the faces of the input shell. The shell must be planar within the input angular tolerance.
1351
-
1352
- Parameters
1353
- ----------
1354
- shell : topologic.Shell
1355
- The input shell.
1356
- angTolerance : float , optional
1357
- The desired angular tolerance. The default is 0.1.
1358
- tolerance : float , optional
1359
- The desired tolerance. The default is 0.0001.
1360
-
1361
- Returns
1362
- -------
1363
- topologic.Face
1364
- The created face.
1365
-
1366
- """
1367
- from topologicpy.Wire import Wire
1368
- from topologicpy.Face import Face
1369
- from topologicpy.Shell import Shell
1370
- from topologicpy.Topology import Topology
1371
-
1372
- def planarizeList(wireList):
1373
- returnList = []
1374
- for aWire in wireList:
1375
- returnList.append(Wire.Planarize(aWire))
1376
- return returnList
1377
- if not isinstance(shell, topologic.Shell):
1378
- return None
1379
- ext_boundary = Shell.ExternalBoundary(shell, tolerance=tolerance)
1380
- if isinstance(ext_boundary, topologic.Wire):
1381
- f = Face.ByWire(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance) or Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance))
1382
- if not f:
1383
- print("FaceByPlanarShell - Error: The input Wire is not planar and could not be fixed. Returning None.")
1384
- return None
1385
- else:
1386
- return f
1387
- elif isinstance(ext_boundary, topologic.Cluster):
1388
- wires = []
1389
- _ = ext_boundary.Wires(None, wires)
1390
- faces = []
1391
- areas = []
1392
- for aWire in wires:
1393
- try:
1394
- aFace = topologic.Face.ByExternalBoundary(Topology.RemoveCollinearEdges(aWire, angTolerance))
1395
- except:
1396
- aFace = topologic.Face.ByExternalBoundary(Wire.Planarize(Topology.RemoveCollinearEdges(aWire, angTolerance)))
1397
- anArea = Face.Area(aFace)
1398
- faces.append(aFace)
1399
- areas.append(anArea)
1400
- max_index = areas.index(max(areas))
1401
- ext_boundary = faces[max_index]
1402
- int_boundaries = list(set(faces) - set([ext_boundary]))
1403
- int_wires = []
1404
- for int_boundary in int_boundaries:
1405
- temp_wires = []
1406
- _ = int_boundary.Wires(None, temp_wires)
1407
- int_wires.append(Topology.RemoveCollinearEdges(temp_wires[0], angTolerance))
1408
- temp_wires = []
1409
- _ = ext_boundary.Wires(None, temp_wires)
1410
- ext_wire = Topology.RemoveCollinearEdges(temp_wires[0], angTolerance)
1411
- try:
1412
- return Face.ByWires(ext_wire, int_wires, tolerance=tolerance)
1413
- except:
1414
- return Face.ByWires(Wire.Planarize(ext_wire), planarizeList(int_wires), tolerance=tolerance)
1415
- else:
1416
- return None
1417
-
1418
- def Skeleton(face, tolerance: float = 0.001):
1419
- """
1420
- Creates a shell through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
1421
- This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
1422
-
1423
- Parameters
1424
- ----------
1425
- face : topologic.Face
1426
- The input face.
1427
- tolerance : float , optional
1428
- The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
1429
-
1430
- Returns
1431
- -------
1432
- topologic.Shell
1433
- The created straight skeleton.
1434
-
1435
- """
1436
- from topologicpy.Wire import Wire
1437
- from topologicpy.Face import Face
1438
- from topologicpy.Topology import Topology
1439
- import topologic
1440
- import math
1441
-
1442
- if not isinstance(face, topologic.Face):
1443
- return None
1444
- roof = Wire.Skeleton(face, tolerance=tolerance)
1445
- if not roof:
1446
- return None
1447
- br = Wire.BoundingRectangle(roof) #This works even if it is a Cluster not a Wire
1448
- br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1)
1449
- bf = Face.ByWire(br, tolerance=tolerance)
1450
- large_shell = Topology.Boolean(bf, roof, operation="slice", tolerance=tolerance)
1451
- if not large_shell:
1452
- return None
1453
- faces = Topology.Faces(large_shell)
1454
- if not faces:
1455
- return None
1456
- final_faces = []
1457
- for f in faces:
1458
- internalBoundaries = Face.InternalBoundaries(f)
1459
- if len(internalBoundaries) == 0:
1460
- final_faces.append(f)
1461
- shell = Shell.ByFaces(final_faces, tolerance=tolerance)
1462
- return shell
1463
-
1464
- @staticmethod
1465
- def Simplify(shell, simplifyBoundary=True, tolerance=0.0001):
1466
- """
1467
- Simplifies the input shell edges based on the Douglas Peucker algorthim. See https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
1468
- Part of this code was contributed by gaoxipeng. See https://github.com/wassimj/topologicpy/issues/35
1469
-
1470
- Parameters
1471
- ----------
1472
- shell : topologic.Shell
1473
- The input shell.
1474
- simplifyBoundary : bool , optional
1475
- If set to True, the external boundary of the shell will be simplified as well. Otherwise, it will not be simplified. The default is True.
1476
- tolerance : float , optional
1477
- The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed.
1478
-
1479
- Returns
1480
- -------
1481
- topologic.Shell
1482
- The simplified shell.
1483
-
1484
- """
1485
- from topologicpy.Vertex import Vertex
1486
- from topologicpy.Wire import Wire
1487
- from topologicpy.Face import Face
1488
- from topologicpy.Shell import Shell
1489
- from topologicpy.Cluster import Cluster
1490
- from topologicpy.Topology import Topology
1491
- from topologicpy.Helper import Helper
1492
-
1493
- def perpendicular_distance(point, line_start, line_end):
1494
- # Calculate the perpendicular distance from a point to a line segment
1495
- x0 = point.X()
1496
- y0 = point.Y()
1497
- x1 = line_start.X()
1498
- y1 = line_start.Y()
1499
- x2 = line_end.X()
1500
- y2 = line_end.Y()
1501
-
1502
- numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
1503
- denominator = Vertex.Distance(line_start, line_end)
1504
-
1505
- return numerator / denominator
1506
-
1507
- def douglas_peucker(wire, tolerance):
1508
- if isinstance(wire, list):
1509
- points = wire
1510
- else:
1511
- points = Wire.Vertices(wire)
1512
- # points.insert(0, points.pop())
1513
- if len(points) <= 2:
1514
- return points
1515
-
1516
- # Use the first and last points in the list as the starting and ending points
1517
- start_point = points[0]
1518
- end_point = points[-1]
1519
-
1520
- # Find the point with the maximum distance
1521
- max_distance = 0
1522
- max_index = 0
1523
-
1524
- for i in range(1, len(points) - 1):
1525
- d = perpendicular_distance(points[i], start_point, end_point)
1526
- if d > max_distance:
1527
- max_distance = d
1528
- max_index = i
1529
-
1530
- # If the maximum distance is less than the tolerance, no further simplification is needed
1531
- if max_distance <= tolerance:
1532
- return [start_point, end_point]
1533
-
1534
- # Recursively simplify
1535
- first_segment = douglas_peucker(points[:max_index + 1], tolerance)
1536
- second_segment = douglas_peucker(points[max_index:], tolerance)
1537
-
1538
- # Merge the two simplified segments
1539
- return first_segment[:-1] + second_segment
1540
- if not isinstance(shell, topologic.Shell):
1541
- print("Shell.Simplify - Error: The input shell parameter is not a valid topologic shell. Returning None.")
1542
- return None
1543
- # Get the external boundary of the shell. This can be simplified as well, but might cause issues at the end.
1544
- # At this point, it is assumed to be left as is.
1545
- all_edges = Topology.Edges(shell)
1546
- if simplifyBoundary == False:
1547
- ext_boundary = Face.ByWire(Shell.ExternalBoundary(shell, tolerance=tolerance), tolerance=tolerance)
1548
-
1549
- # Get the internal edges of the shell.
1550
- i_edges = []
1551
- for edge in all_edges:
1552
- faces = Topology.SuperTopologies(edge, shell, topologyType="face")
1553
- if len(faces) > 1: # This means that the edge separates two faces so it is internal.
1554
- i_edges.append(edge)
1555
- # Creat a Wire from the internal edges
1556
- wire = Topology.SelfMerge(Cluster.ByTopologies(i_edges), tolerance=tolerance)
1557
- else:
1558
- wire = Topology.SelfMerge(Cluster.ByTopologies(all_edges), tolerance=tolerance)
1559
- # Split the wires at its junctions (where more than two edges meet at a vertex)
1560
- components = Wire.Split(wire)
1561
- separators = []
1562
- wires = []
1563
- for component in components:
1564
- if isinstance(component, topologic.Cluster):
1565
- component = Topology.SelfMerge(component, tolerance=tolerance)
1566
- if isinstance(component, topologic.Cluster):
1567
- separators.append(Cluster.FreeEdges(component, tolerance=tolerance))
1568
- wires.append(Cluster.FreeWires(component, tolerance=tolerance))
1569
- if isinstance(component, topologic.Edge):
1570
- separators.append(component)
1571
- if isinstance(component, topologic.Wire):
1572
- wires.append(component)
1573
- if isinstance(component, topologic.Edge):
1574
- separators.append(component)
1575
- if isinstance(component, topologic.Wire):
1576
- wires.append(component)
1577
- wires = Helper.Flatten(wires)
1578
- separators = Helper.Flatten(separators)
1579
- results = []
1580
- for w in wires:
1581
- temp_wire = Wire.ByVertices(douglas_peucker(w, tolerance), close=False)
1582
- results.append(temp_wire)
1583
- # Make a Cluster out of the results
1584
- cluster = Cluster.ByTopologies(results)
1585
- # Get all the edges of the result
1586
- edges = Topology.Edges(cluster)
1587
- # Add them to the final edges
1588
- final_edges = edges + separators
1589
- # Make a Cluster out of the final set of edges
1590
- cluster = Cluster.ByTopologies(final_edges)
1591
- if simplifyBoundary == False:
1592
- # Slice the external boundary of the shell by the cluster
1593
- final_result = Topology.Slice(ext_boundary, cluster, tolerance=tolerance)
1594
- else:
1595
- br = Wire.BoundingRectangle(shell)
1596
- br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1.5)
1597
- br = Face.ByWire(br, tolerance=tolerance)
1598
- v = Face.VertexByParameters(br, 0.1, 0.1)
1599
- result = Topology.Slice(br, cluster, tolerance=tolerance)
1600
- faces = Topology.Faces(result)
1601
- final_faces = []
1602
- for face in faces:
1603
- if not Vertex.IsInternal(v, face, tolerance=0.01):
1604
- final_faces.append(face)
1605
- final_result = Shell.ByFaces(final_faces, tolerance=tolerance)
1606
- return final_result
1607
-
1608
-
1609
- @staticmethod
1610
- def Vertices(shell: topologic.Shell) -> list:
1611
- """
1612
- Returns the vertices of the input shell.
1613
-
1614
- Parameters
1615
- ----------
1616
- shell : topologic.Shell
1617
- The input shell.
1618
-
1619
- Returns
1620
- -------
1621
- list
1622
- The list of vertices.
1623
-
1624
- """
1625
- if not isinstance(shell, topologic.Shell):
1626
- return None
1627
- vertices = []
1628
- _ = shell.Vertices(None, vertices)
1629
- return vertices
1630
-
1631
- @staticmethod
1632
- def Voronoi(vertices: list, face: topologic.Face = None, tolerance: float = 0.0001) -> topologic.Shell:
1633
- """
1634
- Returns a voronoi partitioning of the input face based on the input vertices. The vertices must be coplanar and within the face. See https://en.wikipedia.org/wiki/Voronoi_diagram.
1635
-
1636
- Parameters
1637
- ----------
1638
- vertices : list
1639
- The input list of vertices.
1640
- face : topologic.Face , optional
1641
- The input face. If the face is not set an optimised bounding rectangle of the input vertices is used instead. The default is None.
1642
- tolerance : float , optional
1643
- The desired tolerance. The default is 0.0001.
1644
-
1645
- Returns
1646
- -------
1647
- shell
1648
- A shell representing the voronoi partitioning of the input face.
1649
-
1650
- """
1651
- from topologicpy.Vertex import Vertex
1652
- from topologicpy.Edge import Edge
1653
- from topologicpy.Wire import Wire
1654
- from topologicpy.Face import Face
1655
- from topologicpy.Cluster import Cluster
1656
- from topologicpy.Topology import Topology
1657
- from topologicpy.Dictionary import Dictionary
1658
-
1659
- if not isinstance(face, topologic.Face):
1660
- cluster = Cluster.ByTopologies(vertices)
1661
- br = Wire.BoundingRectangle(cluster, optimize=5)
1662
- face = Face.ByWire(br, tolerance=tolerance)
1663
- if not isinstance(vertices, list):
1664
- return None
1665
- vertices = [x for x in vertices if isinstance(x, topologic.Vertex)]
1666
- if len(vertices) < 2:
1667
- return None
1668
-
1669
- # Flatten the input face
1670
- origin = Topology.Centroid(face)
1671
- normal = Face.Normal(face)
1672
- flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1673
- eb = Face.ExternalBoundary(flatFace)
1674
- ibList = Face.InternalBoundaries(flatFace)
1675
- temp_verts = Topology.Vertices(eb)
1676
- new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts]
1677
- eb = Wire.ByVertices(new_verts, close=True)
1678
- new_ibList = []
1679
- for ib in ibList:
1680
- temp_verts = Topology.Vertices(ib)
1681
- new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts]
1682
- new_ibList.append(Wire.ByVertices(new_verts, close=True))
1683
- flatFace = Face.ByWires(eb, new_ibList)
1684
-
1685
- # Create a cluster of the input vertices
1686
- verticesCluster = Cluster.ByTopologies(vertices)
1687
-
1688
- # Flatten the cluster using the same transformations
1689
- verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal)
1690
- flatVertices = Topology.Vertices(verticesCluster)
1691
- flatVertices = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in flatVertices]
1692
- points = []
1693
- for flatVertex in flatVertices:
1694
- points.append([flatVertex.X(), flatVertex.Y()])
1695
-
1696
- br = Wire.BoundingRectangle(flatFace)
1697
- br_vertices = Wire.Vertices(br)
1698
- br_x = []
1699
- br_y = []
1700
- for br_v in br_vertices:
1701
- x, y = Vertex.Coordinates(br_v, outputType="xy")
1702
- br_x.append(x)
1703
- br_y.append(y)
1704
- min_x = min(br_x)
1705
- max_x = max(br_x)
1706
- min_y = min(br_y)
1707
- max_y = max(br_y)
1708
- br_width = abs(max_x - min_x)
1709
- br_length = abs(max_y - min_y)
1710
-
1711
- points.append((-br_width*4, -br_length*4))
1712
- points.append((-br_width*4, br_length*4))
1713
- points.append((br_width*4, -br_length*4))
1714
- points.append((br_width*4, br_length*4))
1715
-
1716
- voronoi = Voronoi(points, furthest_site=False)
1717
- voronoiVertices = []
1718
- for v in voronoi.vertices:
1719
- voronoiVertices.append(Vertex.ByCoordinates(v[0], v[1], 0))
1720
-
1721
- faces = []
1722
- for region in voronoi.regions:
1723
- tempWire = []
1724
- if len(region) > 1 and not -1 in region:
1725
- for v in region:
1726
- tempWire.append(Vertex.ByCoordinates(voronoiVertices[v].X(), voronoiVertices[v].Y(), 0))
1727
- temp_verts = []
1728
- for v in tempWire:
1729
- if len(temp_verts) == 0:
1730
- temp_verts.append(v)
1731
- elif Vertex.Index(v, temp_verts) == None:
1732
- temp_verts.append(v)
1733
- tempWire = temp_verts
1734
- temp_w = Wire.ByVertices(tempWire, close=True)
1735
- faces.append(Face.ByWire(Wire.ByVertices(tempWire, close=True), tolerance=tolerance))
1736
- shell = Shell.ByFaces(faces, tolerance=tolerance)
1737
- edges = Shell.Edges(shell)
1738
- edgesCluster = Cluster.ByTopologies(edges)
1739
- shell = Topology.Slice(flatFace,edgesCluster, tolerance=tolerance)
1740
- shell = Topology.Unflatten(shell, origin=origin, direction=normal)
1741
- return shell
1742
-
1743
- @staticmethod
1744
- def Wires(shell: topologic.Shell) -> list:
1745
- """
1746
- Returns the wires of the input shell.
1747
-
1748
- Parameters
1749
- ----------
1750
- shell : topologic.Shell
1751
- The input shell.
1752
-
1753
- Returns
1754
- -------
1755
- list
1756
- The list of wires.
1757
-
1758
- """
1759
- if not isinstance(shell, topologic.Shell):
1760
- return None
1761
- wires = []
1762
- _ = shell.Wires(None, wires)
1763
- return wires
1764
-
1765
-
1766
-
1767
-
1768
-
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.Topology import Topology
19
+ import math
20
+ import os
21
+ import warnings
22
+
23
+ try:
24
+ from tqdm.auto import tqdm
25
+ except:
26
+ print("Shell - Installing required tqdm library.")
27
+ try:
28
+ os.system("pip install tqdm")
29
+ except:
30
+ os.system("pip install tqdm --user")
31
+ try:
32
+ from tqdm.auto import tqdm
33
+ print("Shell - tqdm library installed correctly.")
34
+ except:
35
+ warnings.warn("Shell - Error: Could not import tqdm.")
36
+
37
+ try:
38
+ from scipy.spatial import Delaunay
39
+ from scipy.spatial import Voronoi
40
+ except:
41
+ print("Shell - Install required scipy library.")
42
+ try:
43
+ os.system("pip install scipy")
44
+ except:
45
+ os.system("pip install scipy --user")
46
+ try:
47
+ from scipy.spatial import Delaunay
48
+ from scipy.spatial import Voronoi
49
+ except:
50
+ warnings.warn("Shell - Error: Could not import scipy.")
51
+
52
+ class Shell(Topology):
53
+ @staticmethod
54
+ def ByDisjointFaces(externalBoundary, faces, maximumGap=0.5, mergeJunctions=False, threshold=0.5, uSides=1, vSides=1, transferDictionaries=False, tolerance=0.0001):
55
+ """
56
+ Creates a shell from an input list of disjointed faces. THIS IS STILL EXPERIMENTAL
57
+
58
+ Parameters
59
+ ----------
60
+ externalBoundary : topologic.Face
61
+ The input external boundary of the faces. This resembles a ribbon (face with hole) where its interior boundary touches the edges of the input list of faces.
62
+ faces : list
63
+ The input list of faces.
64
+ maximumGap : float , optional
65
+ The length of the maximum gap between the faces. The default is 0.5.
66
+ mergeJunctions : bool , optional
67
+ If set to True, the interior junctions are merged into a single vertex. Otherwise, diagonal edges are added to resolve transitions between different gap distances.
68
+ threshold : float , optional
69
+ The desired threshold under which vertices are merged into a single vertex. The default is 0.5.
70
+ uSides : int , optional
71
+ The desired number of sides along the X axis for the grid that subdivides the input faces to aid in processing. The default is 1.
72
+ vSides : int , optional
73
+ The desired number of sides along the Y axis for the grid that subdivides the input faces to aid in processing. The default is 1.
74
+ transferDictionaries : bool, optional.
75
+ If set to True, the dictionaries in the input list of faces are transfered to the faces of the resulting shell. The default is False.
76
+ tolerance : float , optional
77
+ The desired tolerance. The default is 0.0001.
78
+
79
+ Returns
80
+ -------
81
+ topologic.Shell
82
+ The created Shell.
83
+
84
+ """
85
+ from topologicpy.Vertex import Vertex
86
+ from topologicpy.Edge import Edge
87
+ from topologicpy.Wire import Wire
88
+ from topologicpy.Face import Face
89
+ from topologicpy.Cluster import Cluster
90
+ from topologicpy.Helper import Helper
91
+ from topologicpy.Topology import Topology
92
+ from topologicpy.Grid import Grid
93
+ from topologicpy.Dictionary import Dictionary
94
+
95
+ def removeShards(edges, hostTopology, maximumGap=0.5):
96
+ returnEdges = []
97
+ for e in tqdm(edges, desc="Removing Shards", leave=False):
98
+ if Edge.Length(e) < maximumGap:
99
+ sv = Edge.StartVertex(e)
100
+ ev = Edge.EndVertex(e)
101
+ sEdges = Topology.SuperTopologies(sv, hostTopology, "edge")
102
+ sn = len(sEdges)
103
+ eEdges = Topology.SuperTopologies(ev, hostTopology, "edge")
104
+ en = len(eEdges)
105
+ if sn >= 2 and en >= 2:
106
+ returnEdges.append(e)
107
+ else:
108
+ returnEdges.append(e)
109
+ return returnEdges
110
+
111
+ def extendEdges(edges, hostTopology, maximumGap=0.5):
112
+ returnEdges = []
113
+ for e in tqdm(edges, desc="Extending Edges", leave=False):
114
+ sv = Edge.StartVertex(e)
115
+ ev = Edge.EndVertex(e)
116
+ sEdges = Topology.SuperTopologies(sv, hostTopology, "edge")
117
+ sn = len(sEdges)
118
+ eEdges = Topology.SuperTopologies(ev, hostTopology, "edge")
119
+ en = len(eEdges)
120
+ if sn == 1:
121
+ ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=True)
122
+ returnEdges.append(ee)
123
+ elif en == 1:
124
+ ee = Edge.Extend(e, distance=maximumGap, bothSides=False, reverse=False)
125
+ returnEdges.append(ee)
126
+ else:
127
+ returnEdges.append(e)
128
+ return returnEdges
129
+
130
+ facesCluster = Cluster.ByTopologies(faces)
131
+ internalBoundary = Face.ByWire(Face.InternalBoundaries(externalBoundary)[0], tolerance=tolerance)
132
+ bb = Topology.BoundingBox(internalBoundary)
133
+ bb_d = Topology.Dictionary(bb)
134
+ unitU = Dictionary.ValueAtKey(bb_d, 'width') / uSides
135
+ unitV = Dictionary.ValueAtKey(bb_d, 'length') / vSides
136
+ uRange = [u*unitU for u in range(uSides)]
137
+ vRange = [v*unitV for v in range(vSides)]
138
+ grid = Grid.EdgesByDistances(internalBoundary, uRange=uRange, vRange=vRange, clip=True)
139
+ grid = Topology.Slice(internalBoundary, grid, tolerance=tolerance)
140
+ grid_faces = Topology.Faces(grid)
141
+ skeletons = []
142
+ for ib in tqdm(grid_faces, desc="Processing "+str(len(grid_faces))+" tiles", leave=False):
143
+ building_shell = Topology.Slice(ib, facesCluster, tolerance=tolerance)
144
+ wall_faces = Topology.Faces(building_shell)
145
+ walls = []
146
+ for w1 in wall_faces:
147
+ iv = Topology.InternalVertex(w1, tolerance=tolerance)
148
+ flag = False
149
+ for w2 in faces:
150
+ if Vertex.IsInternal(iv, w2):
151
+ flag = True
152
+ break;
153
+ if flag == False:
154
+ walls.append(w1)
155
+ for wall in walls:
156
+ skeleton = Wire.Skeleton(wall, tolerance=0.001) # This tolerance works better.
157
+ skeleton = Topology.Difference(skeleton, facesCluster, tolerance=tolerance)
158
+ skeleton = Topology.Difference(skeleton, Face.Wire(wall), tolerance=tolerance)
159
+ skeletons.append(skeleton)
160
+ if len(skeletons) > 0:
161
+ skeleton_cluster = Cluster.ByTopologies(skeletons+[internalBoundary])
162
+ skEdges = Topology.SelfMerge(Cluster.ByTopologies(removeShards(Topology.Edges(skeleton_cluster), skeleton_cluster, maximumGap=maximumGap)), tolerance=tolerance)
163
+ if isinstance(skEdges, topologic.Edge):
164
+ skEdges = extendEdges([skEdges], skEdges, maximumGap=maximumGap)
165
+ else:
166
+ skEdges = extendEdges(Topology.Edges(skEdges), skEdges, maximumGap=maximumGap)
167
+ if len(skEdges) < 1:
168
+ print("ShellByDisjointFaces - Warning: No edges were extended.")
169
+ #return Cluster.ByTopologies(skEdges)
170
+ #print("ShellByDisjointFaces - Error: Could not derive central skeleton of interior walls. Returning None.")
171
+ #return None
172
+
173
+ shell = Topology.Slice(internalBoundary, skeleton_cluster, tolerance=tolerance)
174
+ if mergeJunctions == True:
175
+ vertices = Shell.Vertices(shell)
176
+ centers = []
177
+ used = []
178
+ for v in vertices:
179
+ for w in vertices:
180
+ if not Topology.IsSame(v, w) and not w in used:
181
+ if Vertex.Distance(v, w) < threshold:
182
+ centers.append(v)
183
+ used.append(w)
184
+ edges = Shell.Edges(shell)
185
+ new_edges = []
186
+ for e in edges:
187
+ sv = Edge.StartVertex(e)
188
+ ev = Edge.EndVertex(e)
189
+ for v in centers:
190
+ if Vertex.Distance(sv, v) < threshold:
191
+ sv = v
192
+ if Vertex.Distance(ev, v) < threshold:
193
+ ev = v
194
+ new_edges.append(Edge.ByVertices([sv,ev], tolerance=tolerance))
195
+ cluster = Cluster.ByTopologies(new_edges)
196
+
197
+ vertices = Topology.Vertices(cluster)
198
+ edges = Topology.Edges(shell)
199
+
200
+ xList = list(set([Vertex.X(v) for v in vertices]))
201
+ xList.sort()
202
+ xList = Helper.MergeByThreshold(xList, 0.5)
203
+ yList = list(set([Vertex.Y(v) for v in vertices]))
204
+ yList.sort()
205
+ yList = Helper.MergeByThreshold(yList, 0.5)
206
+ yList.sort()
207
+
208
+ centers = []
209
+
210
+ new_edges = []
211
+
212
+ for e in edges:
213
+ sv = Edge.StartVertex(e)
214
+ ev = Edge.EndVertex(e)
215
+ svx = Vertex.X(sv)
216
+ svy = Vertex.Y(sv)
217
+ evx = Vertex.X(ev)
218
+ evy = Vertex.Y(ev)
219
+ for x in xList:
220
+ if abs(svx-x) < threshold:
221
+ svx = x
222
+ break;
223
+ for y in yList:
224
+ if abs(svy-y) < threshold:
225
+ svy = y
226
+ break;
227
+ sv = Vertex.ByCoordinates(svx, svy, 0)
228
+ for x in xList:
229
+ if abs(evx-x) < threshold:
230
+ evx = x
231
+ break;
232
+ for y in yList:
233
+ if abs(evy-y) < threshold:
234
+ evy = y
235
+ break;
236
+ sv = Vertex.ByCoordinates(svx, svy, 0)
237
+ ev = Vertex.ByCoordinates(evx, evy, 0)
238
+ new_edges.append(Edge.ByVertices([sv, ev], tolerance=tolerance))
239
+
240
+ cluster = Cluster.ByTopologies(new_edges)
241
+ eb = Face.ByWire(Shell.ExternalBoundary(shell), tolerance=tolerance)
242
+ shell = Topology.Slice(eb, cluster, tolerance=tolerance)
243
+ if not isinstance(shell, topologic.Shell):
244
+ try:
245
+ temp_wires = [Wire.RemoveCollinearEdges(w, angTolerance=1.0) for w in Topology.Wires(shell)]
246
+ temp_faces = [Face.ByWire(w, tolerance=tolerance) for w in temp_wires]
247
+ except:
248
+ temp_faces = Topology.Faces(shell)
249
+ shell = Shell.ByFaces(temp_faces, tolerance=tolerance)
250
+ if transferDictionaries == True:
251
+ selectors = []
252
+ for f in faces:
253
+ d = Topology.Dictionary(f)
254
+ s = Topology.InternalVertex(f, tolerance=tolerance)
255
+ s = Topology.SetDictionary(s, d)
256
+ selectors.append(s)
257
+ _ = Topology.TransferDictionariesBySelectors(topology=shell, selectors=selectors, tranFaces=True, tolerance=tolerance)
258
+ return shell
259
+ return None
260
+
261
+ @staticmethod
262
+ def ByFaces(faces: list, tolerance: float = 0.0001) -> topologic.Shell:
263
+ """
264
+ Creates a shell from the input list of faces.
265
+
266
+ Parameters
267
+ ----------
268
+ faces : list
269
+ The input list of faces.
270
+ tolerance : float , optional
271
+ The desired tolerance. The default is 0.0001.
272
+
273
+ Returns
274
+ -------
275
+ topologic.Shell
276
+ The created Shell.
277
+
278
+ """
279
+ from topologicpy.Topology import Topology
280
+
281
+ if not isinstance(faces, list):
282
+ return None
283
+ faceList = [x for x in faces if isinstance(x, topologic.Face)]
284
+ if len(faceList) == 0:
285
+ print("Shell.ByFaces - Error: The input faces list does not contain any valid faces. Returning None.")
286
+ return None
287
+ shell = topologic.Shell.ByFaces(faceList, tolerance)
288
+ if not isinstance(shell, topologic.Shell):
289
+ shell = Topology.SelfMerge(shell, tolerance=tolerance)
290
+ if isinstance(shell, topologic.Shell):
291
+ return shell
292
+ else:
293
+ print("Shell.ByFaces - Error: Could not create shell. Returning None.")
294
+ return None
295
+ else:
296
+ return shell
297
+
298
+ @staticmethod
299
+ def ByFacesCluster(cluster: topologic.Cluster, tolerance: float = 0.0001) -> topologic.Shell:
300
+ """
301
+ Creates a shell from the input cluster of faces.
302
+
303
+ Parameters
304
+ ----------
305
+ cluster : topologic.Cluster
306
+ The input cluster of faces.
307
+ tolerance : float , optional
308
+ The desired tolerance. The default is 0.0001.
309
+
310
+ Returns
311
+ -------
312
+ topologic.Shell
313
+ The created shell.
314
+
315
+ """
316
+ if not isinstance(cluster, topologic.Cluster):
317
+ return None
318
+ faces = []
319
+ _ = cluster.Faces(None, faces)
320
+ return Shell.ByFaces(faces, tolerance=tolerance)
321
+
322
+ @staticmethod
323
+ def ByWires(wires: list, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False) -> topologic.Shell:
324
+ """
325
+ Creates a shell by lofting through the input wires
326
+ Parameters
327
+ ----------
328
+ wires : list
329
+ The input list of wires.
330
+ triangulate : bool , optional
331
+ If set to True, the faces will be triangulated. The default is True.
332
+ tolerance : float , optional
333
+ The desired tolerance. The default is 0.0001.
334
+ silent : bool , optional
335
+ If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
336
+
337
+ Returns
338
+ -------
339
+ topologic.Shell
340
+ The creates shell.
341
+ """
342
+ from topologicpy.Edge import Edge
343
+ from topologicpy.Wire import Wire
344
+ from topologicpy.Face import Face
345
+ if not isinstance(wires, list):
346
+ return None
347
+ wireList = [x for x in wires if isinstance(x, topologic.Wire)]
348
+ faces = []
349
+ for i in range(len(wireList)-1):
350
+ wire1 = wireList[i]
351
+ wire2 = wireList[i+1]
352
+ if wire1.Type() < topologic.Edge.Type() or wire2.Type() < topologic.Edge.Type():
353
+ return None
354
+ if wire1.Type() == topologic.Edge.Type():
355
+ w1_edges = [wire1]
356
+ else:
357
+ w1_edges = []
358
+ _ = wire1.Edges(None, w1_edges)
359
+ if wire2.Type() == topologic.Edge.Type():
360
+ w2_edges = [wire2]
361
+ else:
362
+ w2_edges = []
363
+ _ = wire2.Edges(None, w2_edges)
364
+ if len(w1_edges) != len(w2_edges):
365
+ return None
366
+ if triangulate == True:
367
+ for j in range (len(w1_edges)):
368
+ e1 = w1_edges[j]
369
+ e2 = w2_edges[j]
370
+ e3 = None
371
+ e4 = None
372
+ try:
373
+ e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
374
+ except:
375
+ e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
376
+ faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e4], tolerance=tolerance), tolerance=tolerance))
377
+ try:
378
+ e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
379
+ except:
380
+ e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
381
+ faces.append(Face.ByWire(Wire.ByEdges([e1, e2, e3],tolerance=tolerance), tolerance=tolerance))
382
+ if e3 and e4:
383
+ e5 = Edge.ByVertices([e1.StartVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
384
+ faces.append(Face.ByWire(Wire.ByEdges([e1, e5, e4], tolerance=tolerance), tolerance=tolerance))
385
+ faces.append(Face.ByWire(Wire.ByEdges([e2, e5, e3], tolerance=tolerance), tolerance=tolerance))
386
+ else:
387
+ for j in range (len(w1_edges)):
388
+ e1 = w1_edges[j]
389
+ e2 = w2_edges[j]
390
+ e3 = None
391
+ e4 = None
392
+ try:
393
+ e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
394
+ except:
395
+ try:
396
+ e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
397
+ except:
398
+ pass
399
+ try:
400
+ e4 = Edge.ByVertices([e1.EndVertex(), e2.EndVertex()], tolerance=tolerance, silent=silent)
401
+ except:
402
+ try:
403
+ e3 = Edge.ByVertices([e1.StartVertex(), e2.StartVertex()], tolerance=tolerance, silent=silent)
404
+ except:
405
+ pass
406
+ if e3 and e4:
407
+ try:
408
+ faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2, e3], tolerance=tolerance), tolerance=tolerance))
409
+ except:
410
+ faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2, e4], tolerance=tolerance), tolerance=tolerance))
411
+ elif e3:
412
+ faces.append(Face.ByWire(Wire.ByEdges([e1, e3, e2], tolerance=tolerance), tolerance=tolerance))
413
+ elif e4:
414
+ faces.append(Face.ByWire(Wire.ByEdges([e1, e4, e2], tolerance=tolerance), tolerance=tolerance))
415
+ return Shell.ByFaces(faces, tolerance=tolerance)
416
+
417
+ @staticmethod
418
+ def ByWiresCluster(cluster: topologic.Cluster, triangulate: bool = True, tolerance: float = 0.0001, silent: bool = False) -> topologic.Shell:
419
+ """
420
+ Creates a shell by lofting through the input cluster of wires
421
+
422
+ Parameters
423
+ ----------
424
+ wires : topologic.Cluster
425
+ The input cluster of wires.
426
+ triangulate : bool , optional
427
+ If set to True, the faces will be triangulated. The default is True.
428
+ tolerance : float , optional
429
+ The desired tolerance. The default is 0.0001.
430
+ silent : bool , optional
431
+ If set to False, error and warning messages are printed. Otherwise, they are not. The default is False.
432
+
433
+ Returns
434
+ -------
435
+ topologic.Shell
436
+ The creates shell.
437
+
438
+ """
439
+ from topologicpy.Cluster import Cluster
440
+ if not cluster:
441
+ return None
442
+ if not isinstance(cluster, topologic.Cluster):
443
+ return None
444
+ wires = Cluster.Wires(cluster)
445
+ return Shell.ByWires(wires, triangulate=triangulate, tolerance=tolerance, silent=silent)
446
+
447
+ @staticmethod
448
+ def Circle(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 32, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
449
+ """
450
+ Creates a circle.
451
+
452
+ Parameters
453
+ ----------
454
+ origin : topologic.Vertex , optional
455
+ The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
456
+ radius : float , optional
457
+ The radius of the circle. The default is 0.5.
458
+ sides : int , optional
459
+ The number of sides of the circle. The default is 32.
460
+ fromAngle : float , optional
461
+ The angle in degrees from which to start creating the arc of the circle. The default is 0.
462
+ toAngle : float , optional
463
+ The angle in degrees at which to end creating the arc of the circle. The default is 360.
464
+ direction : list , optional
465
+ The vector representing the up direction of the circle. The default is [0, 0, 1].
466
+ placement : str , optional
467
+ The description of the placement of the origin of the pie. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
468
+ tolerance : float , optional
469
+ The desired tolerance. The default is 0.0001.
470
+
471
+ Returns
472
+ -------
473
+ topologic.Shell
474
+ The created circle.
475
+ """
476
+ return Shell.Pie(origin=origin, radiusA=radius, radiusB=0, sides=sides, rings=1, fromAngle=fromAngle, toAngle=toAngle, direction=direction, placement=placement, tolerance=tolerance)
477
+
478
+ @staticmethod
479
+ def Delaunay(vertices: list, face: topologic.Face = None, tolerance: float = 0.0001) -> topologic.Shell:
480
+ """
481
+ Returns a delaunay partitioning of the input vertices. The vertices must be coplanar. See https://en.wikipedia.org/wiki/Delaunay_triangulation.
482
+
483
+ Parameters
484
+ ----------
485
+ vertices : list
486
+ The input list of vertices.
487
+ face : topologic.Face , optional
488
+ The input face. If specified, the delaunay triangulation is clipped to the face.
489
+ tolerance : float , optional
490
+ The desired tolerance. The default is 0.0001.
491
+
492
+ Returns
493
+ -------
494
+ shell
495
+ A shell representing the delaunay triangulation of the input vertices.
496
+
497
+ """
498
+ from topologicpy.Vertex import Vertex
499
+ from topologicpy.Wire import Wire
500
+ from topologicpy.Face import Face
501
+ from topologicpy.Cluster import Cluster
502
+ from topologicpy.Topology import Topology
503
+ from topologicpy.Dictionary import Dictionary
504
+ from random import sample
505
+ from scipy.spatial import Delaunay as SCIDelaunay
506
+ import numpy as np
507
+
508
+ if not isinstance(vertices, list):
509
+ return None
510
+ vertices = [x for x in vertices if isinstance(x, topologic.Vertex)]
511
+ if len(vertices) < 3:
512
+ return None
513
+
514
+ # Create a Vertex at the world's origin (0, 0, 0)
515
+ world_origin = Vertex.Origin()
516
+
517
+ if isinstance(face, topologic.Face):
518
+ # Flatten the face
519
+ origin = Topology.Centroid(face)
520
+ normal = Face.Normal(face)
521
+ flatFace = Topology.Flatten(face, origin=origin, direction=normal)
522
+ faceVertices = Face.Vertices(face)
523
+ vertices += faceVertices
524
+
525
+ # Create a cluster of the input vertices
526
+ verticesCluster = Cluster.ByTopologies(vertices)
527
+
528
+ # Flatten the cluster using the same transformations
529
+ verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal)
530
+
531
+ vertices = Cluster.Vertices(verticesCluster)
532
+ points = []
533
+ for v in vertices:
534
+ points.append([Vertex.X(v), Vertex.Y(v)])
535
+ delaunay = SCIDelaunay(points)
536
+ simplices = delaunay.simplices
537
+
538
+ faces = []
539
+ for simplex in simplices:
540
+ tempTriangleVertices = []
541
+ tempTriangleVertices.append(vertices[simplex[0]])
542
+ tempTriangleVertices.append(vertices[simplex[1]])
543
+ tempTriangleVertices.append(vertices[simplex[2]])
544
+ tempFace = Face.ByWire(Wire.ByVertices(tempTriangleVertices), tolerance=tolerance)
545
+ faces.append(tempFace)
546
+
547
+ shell = Shell.ByFaces(faces, tolerance=tolerance)
548
+ if shell == None:
549
+ shell = Cluster.ByTopologies(faces)
550
+
551
+ if isinstance(face, topologic.Face):
552
+ edges = Topology.Edges(shell)
553
+ shell = Topology.Slice(flatFace, Cluster.ByTopologies(edges))
554
+ # Get the internal boundaries of the face
555
+ wires = Face.InternalBoundaries(flatFace)
556
+ ibList = []
557
+ if len(wires) > 0:
558
+ ibList = [Face.ByWire(w) for w in wires]
559
+ cluster = Cluster.ByTopologies(ibList)
560
+ shell = Topology.Difference(shell, cluster)
561
+ shell = Topology.Unflatten(shell, origin=origin, direction=normal)
562
+ return shell
563
+
564
+ @staticmethod
565
+ def Edges(shell: topologic.Shell) -> list:
566
+ """
567
+ Returns the edges of the input shell.
568
+
569
+ Parameters
570
+ ----------
571
+ shell : topologic.Shell
572
+ The input shell.
573
+
574
+ Returns
575
+ -------
576
+ list
577
+ The list of edges.
578
+
579
+ """
580
+ if not isinstance(shell, topologic.Shell):
581
+ return None
582
+ edges = []
583
+ _ = shell.Edges(None, edges)
584
+ return edges
585
+
586
+ @staticmethod
587
+ def ExternalBoundary(shell: topologic.Shell, tolerance: float = 0.0001) -> topologic.Wire:
588
+ """
589
+ Returns the external boundary of the input shell.
590
+
591
+ Parameters
592
+ ----------
593
+ shell : topologic.Shell
594
+ The input shell.
595
+ tolerance : float , optional
596
+ The desired tolerance. The default is 0.0001.
597
+
598
+ Returns
599
+ -------
600
+ topologic.Wire or topologic.Cluster
601
+ The external boundary of the input shell. If the shell has holes, the return value will be a cluster of wires.
602
+
603
+ """
604
+ from topologicpy.Wire import Wire
605
+ from topologicpy.Cluster import Cluster
606
+ from topologicpy.Topology import Topology
607
+
608
+ if not isinstance(shell, topologic.Shell):
609
+ return None
610
+ edges = []
611
+ _ = shell.Edges(None, edges)
612
+ obEdges = []
613
+ for anEdge in edges:
614
+ faces = []
615
+ _ = anEdge.Faces(shell, faces)
616
+ if len(faces) == 1:
617
+ obEdges.append(anEdge)
618
+ return Topology.SelfMerge(Cluster.ByTopologies(obEdges), tolerance=tolerance)
619
+
620
+ @staticmethod
621
+ def Faces(shell: topologic.Shell) -> list:
622
+ """
623
+ Returns the faces of the input shell.
624
+
625
+ Parameters
626
+ ----------
627
+ shell : topologic.Shell
628
+ The input shell.
629
+
630
+ Returns
631
+ -------
632
+ list
633
+ The list of faces.
634
+
635
+ """
636
+ if not isinstance(shell, topologic.Shell):
637
+ return None
638
+ faces = []
639
+ _ = shell.Faces(None, faces)
640
+ return faces
641
+
642
+ @staticmethod
643
+ def IsOnBoundary(shell: topologic.Shell, vertex: topologic.Vertex, tolerance: float = 0.0001) -> bool:
644
+ """
645
+ Returns True if the input vertex is on the boundary of the input shell. Returns False otherwise. On the boundary is defined as being on the boundary of one of the shell's external or internal boundaries
646
+
647
+ Parameters
648
+ ----------
649
+ shell : topologic.Shell
650
+ The input shell.
651
+ vertex : topologic.Vertex
652
+ The input vertex.
653
+ tolerance : float , optional
654
+ The desired tolerance. The default is 0.0001.
655
+
656
+ Returns
657
+ -------
658
+ bool
659
+ Returns True if the input vertex is inside the input shell. Returns False otherwise.
660
+
661
+ """
662
+
663
+ from topologicpy.Vertex import Vertex
664
+
665
+ if not isinstance(shell, topologic.Shell):
666
+ return None
667
+ if not isinstance(vertex, topologic.Vertex):
668
+ return None
669
+ boundary = Shell.ExternalBoundary(shell, tolerance=tolerance)
670
+ if Vertex.IsInternal(vertex, boundary, tolerance=tolerance):
671
+ return True
672
+ internal_boundaries = Shell.InternalBoundaries(shell, tolerance=tolerance)
673
+ for ib in internal_boundaries:
674
+ if Vertex.IsInternal(vertex, ib, tolerance=tolerance):
675
+ return True
676
+ return False
677
+
678
+ @staticmethod
679
+ def HyperbolicParaboloidRectangularDomain(origin: topologic.Vertex = None, llVertex: topologic.Vertex = None, lrVertex: topologic.Vertex =None, ulVertex: topologic.Vertex =None, urVertex: topologic.Vertex = None,
680
+ uSides: int = 10, vSides: int = 10, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
681
+ """
682
+ Creates a hyperbolic paraboloid with a rectangular domain.
683
+
684
+ Parameters
685
+ ----------
686
+ origin : topologic.Vertex , optional
687
+ The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
688
+ llVertex : topologic.Vertex , optional
689
+ The lower left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, -0.5, -0.5).
690
+ lrVertex : topologic.Vertex , optional
691
+ The lower right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, -0.5, 0.5).
692
+ ulVertex : topologic.Vertex , optional
693
+ The upper left corner of the hyperbolic parabolid. If set to None, it will be set to (-0.5, 0.5, 0.5).
694
+ urVertex : topologic.Vertex , optional
695
+ The upper right corner of the hyperbolic parabolid. If set to None, it will be set to (0.5, 0.5, -0.5).
696
+ uSides : int , optional
697
+ The number of segments along the X axis. The default is 10.
698
+ vSides : int , optional
699
+ The number of segments along the Y axis. The default is 10.
700
+ direction : list , optional
701
+ The vector representing the up direction of the hyperbolic parabolid. The default is [0, 0, 1].
702
+ placement : str , optional
703
+ The description of the placement of the origin of the hyperbolic parabolid. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center".
704
+ tolerance : float , optional
705
+ The desired tolerance. The default is 0.0001.
706
+ Returns
707
+ -------
708
+ topologic.Shell
709
+ The created hyperbolic paraboloid.
710
+
711
+ """
712
+ from topologicpy.Vertex import Vertex
713
+ from topologicpy.Edge import Edge
714
+ from topologicpy.Face import Face
715
+ from topologicpy.Topology import Topology
716
+ if not isinstance(origin, topologic.Vertex):
717
+ origin = Vertex.ByCoordinates(0, 0, 0)
718
+ if not isinstance(llVertex, topologic.Vertex):
719
+ llVertex = Vertex.ByCoordinates(-0.5, -0.5, -0.5)
720
+ if not isinstance(lrVertex, topologic.Vertex):
721
+ lrVertex = Vertex.ByCoordinates(0.5, -0.5, 0.5)
722
+ if not isinstance(ulVertex, topologic.Vertex):
723
+ ulVertex = Vertex.ByCoordinates(-0.5, 0.5, 0.5)
724
+ if not isinstance(urVertex, topologic.Vertex):
725
+ urVertex = Vertex.ByCoordinates(0.5, 0.5, -0.5)
726
+ e1 = Edge.ByVertices([llVertex, lrVertex], tolerance=tolerance)
727
+ e3 = Edge.ByVertices([urVertex, ulVertex], tolerance=tolerance)
728
+ edges = []
729
+ for i in range(uSides+1):
730
+ v1 = Edge.VertexByParameter(e1, float(i)/float(uSides))
731
+ v2 = Edge.VertexByParameter(e3, 1.0 - float(i)/float(uSides))
732
+ edges.append(Edge.ByVertices([v1, v2], tolerance=tolerance))
733
+ faces = []
734
+ for i in range(uSides):
735
+ for j in range(vSides):
736
+ v1 = Edge.VertexByParameter(edges[i], float(j)/float(vSides))
737
+ v2 = Edge.VertexByParameter(edges[i], float(j+1)/float(vSides))
738
+ v3 = Edge.VertexByParameter(edges[i+1], float(j+1)/float(vSides))
739
+ v4 = Edge.VertexByParameter(edges[i+1], float(j)/float(vSides))
740
+ faces.append(Face.ByVertices([v1, v2, v4]))
741
+ faces.append(Face.ByVertices([v4, v2, v3]))
742
+ returnTopology = Shell.ByFaces(faces, tolerance=tolerance)
743
+ if not returnTopology:
744
+ returnTopology = None
745
+ xOffset = 0
746
+ yOffset = 0
747
+ zOffset = 0
748
+ minX = min([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()])
749
+ maxX = max([llVertex.X(), lrVertex.X(), ulVertex.X(), urVertex.X()])
750
+ minY = min([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()])
751
+ maxY = max([llVertex.Y(), lrVertex.Y(), ulVertex.Y(), urVertex.Y()])
752
+ minZ = min([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()])
753
+ maxZ = max([llVertex.Z(), lrVertex.Z(), ulVertex.Z(), urVertex.Z()])
754
+ if placement.lower() == "lowerleft":
755
+ xOffset = -minX
756
+ yOffset = -minY
757
+ zOffset = -minZ
758
+ elif placement.lower() == "bottom":
759
+ xOffset = -(minX + (maxX - minX)*0.5)
760
+ yOffset = -(minY + (maxY - minY)*0.5)
761
+ zOffset = -minZ
762
+ elif placement.lower() == "center":
763
+ xOffset = -(minX + (maxX - minX)*0.5)
764
+ yOffset = -(minY + (maxY - minY)*0.5)
765
+ zOffset = -(minZ + (maxZ - minZ)*0.5)
766
+ returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset)
767
+ returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin)
768
+ returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction)
769
+ return returnTopology
770
+
771
+ @staticmethod
772
+ def HyperbolicParaboloidCircularDomain(origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 36, rings: int = 10,
773
+ A: float = 2.0, B: float = -2.0, direction: list = [0, 0, 1],
774
+ placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
775
+ """
776
+ Creates a hyperbolic paraboloid with a circular domain. See https://en.wikipedia.org/wiki/Compactness_measure_of_a_shape
777
+
778
+ Parameters
779
+ ----------
780
+ origin : topologic.Vertex , optional
781
+ The origin of the hyperbolic parabolid. If set to None, it will be placed at the (0, 0, 0) origin. The default is None.
782
+ radius : float , optional
783
+ The desired radius of the hyperbolic paraboloid. The default is 0.5.
784
+ sides : int , optional
785
+ The desired number of sides of the hyperbolic parabolid. The default is 36.
786
+ rings : int , optional
787
+ The desired number of concentric rings of the hyperbolic parabolid. The default is 10.
788
+ A : float , optional
789
+ The *A* constant in the equation z = A*x^2^ + B*y^2^. The default is 2.0.
790
+ B : float , optional
791
+ The *B* constant in the equation z = A*x^2^ + B*y^2^. The default is -2.0.
792
+ direction : list , optional
793
+ The vector representing the up direction of the hyperbolic paraboloid. The default is [0, 0, 1].
794
+ placement : str , optional
795
+ The description of the placement of the origin of the circle. This can be "center", "lowerleft", "bottom". It is case insensitive. The default is "center".
796
+ tolerance : float , optional
797
+ The desired tolerance. The default is 0.0001.
798
+ Returns
799
+ -------
800
+ topologic.Shell
801
+ The created hyperboloic paraboloid.
802
+
803
+ """
804
+ from topologicpy.Vertex import Vertex
805
+ from topologicpy.Face import Face
806
+ from topologicpy.Topology import Topology
807
+ if not isinstance(origin, topologic.Vertex):
808
+ origin = Vertex.ByCoordinates(0, 0, 0)
809
+ uOffset = float(360)/float(sides)
810
+ vOffset = float(radius)/float(rings)
811
+ faces = []
812
+ for i in range(rings-1):
813
+ r1 = radius - vOffset*i
814
+ r2 = radius - vOffset*(i+1)
815
+ for j in range(sides-1):
816
+ a1 = math.radians(uOffset)*j
817
+ a2 = math.radians(uOffset)*(j+1)
818
+ x1 = math.sin(a1)*r1
819
+ y1 = math.cos(a1)*r1
820
+ z1 = A*x1*x1 + B*y1*y1
821
+ x2 = math.sin(a1)*r2
822
+ y2 = math.cos(a1)*r2
823
+ z2 = A*x2*x2 + B*y2*y2
824
+ x3 = math.sin(a2)*r2
825
+ y3 = math.cos(a2)*r2
826
+ z3 = A*x3*x3 + B*y3*y3
827
+ x4 = math.sin(a2)*r1
828
+ y4 = math.cos(a2)*r1
829
+ z4 = A*x4*x4 + B*y4*y4
830
+ v1 = topologic.Vertex.ByCoordinates(x1,y1,z1)
831
+ v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
832
+ v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
833
+ v4 = topologic.Vertex.ByCoordinates(x4,y4,z4)
834
+ f1 = Face.ByVertices([v1,v2,v4])
835
+ f2 = Face.ByVertices([v4,v2,v3])
836
+ faces.append(f1)
837
+ faces.append(f2)
838
+ a1 = math.radians(uOffset)*(sides-1)
839
+ a2 = math.radians(360)
840
+ x1 = math.sin(a1)*r1
841
+ y1 = math.cos(a1)*r1
842
+ z1 = A*x1*x1 + B*y1*y1
843
+ x2 = math.sin(a1)*r2
844
+ y2 = math.cos(a1)*r2
845
+ z2 = A*x2*x2 + B*y2*y2
846
+ x3 = math.sin(a2)*r2
847
+ y3 = math.cos(a2)*r2
848
+ z3 = A*x3*x3 + B*y3*y3
849
+ x4 = math.sin(a2)*r1
850
+ y4 = math.cos(a2)*r1
851
+ z4 = A*x4*x4 + B*y4*y4
852
+ v1 = topologic.Vertex.ByCoordinates(x1,y1,z1)
853
+ v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
854
+ v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
855
+ v4 = topologic.Vertex.ByCoordinates(x4,y4,z4)
856
+ f1 = Face.ByVertices([v1,v2,v4])
857
+ f2 = Face.ByVertices([v4,v2,v3])
858
+ faces.append(f1)
859
+ faces.append(f2)
860
+ # Special Case: Center triangles
861
+ r = vOffset
862
+ x1 = 0
863
+ y1 = 0
864
+ z1 = 0
865
+ v1 = topologic.Vertex.ByCoordinates(x1,y1,z1)
866
+ for j in range(sides-1):
867
+ a1 = math.radians(uOffset)*j
868
+ a2 = math.radians(uOffset)*(j+1)
869
+ x2 = math.sin(a1)*r
870
+ y2 = math.cos(a1)*r
871
+ z2 = A*x2*x2 + B*y2*y2
872
+ #z2 = 0
873
+ x3 = math.sin(a2)*r
874
+ y3 = math.cos(a2)*r
875
+ z3 = A*x3*x3 + B*y3*y3
876
+ #z3 = 0
877
+ v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
878
+ v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
879
+ f1 = Face.ByVertices([v2,v1,v3])
880
+ faces.append(f1)
881
+ a1 = math.radians(uOffset)*(sides-1)
882
+ a2 = math.radians(360)
883
+ x2 = math.sin(a1)*r
884
+ y2 = math.cos(a1)*r
885
+ z2 = A*x2*x2 + B*y2*y2
886
+ x3 = math.sin(a2)*r
887
+ y3 = math.cos(a2)*r
888
+ z3 = A*x3*x3 + B*y3*y3
889
+ v2 = topologic.Vertex.ByCoordinates(x2,y2,z2)
890
+ v3 = topologic.Vertex.ByCoordinates(x3,y3,z3)
891
+ f1 = Face.ByVertices([v2,v1,v3])
892
+ faces.append(f1)
893
+ returnTopology = topologic.Shell.ByFaces(faces, tolerance)
894
+ if not returnTopology:
895
+ returnTopology = topologic.Cluster.ByTopologies(faces)
896
+ vertices = []
897
+ _ = returnTopology.Vertices(None, vertices)
898
+ xList = []
899
+ yList = []
900
+ zList = []
901
+ for aVertex in vertices:
902
+ xList.append(aVertex.X())
903
+ yList.append(aVertex.Y())
904
+ zList.append(aVertex.Z())
905
+ minX = min(xList)
906
+ maxX = max(xList)
907
+ minY = min(yList)
908
+ maxY = max(yList)
909
+ minZ = min(zList)
910
+ maxZ = max(zList)
911
+ xOffset = 0
912
+ yOffset = 0
913
+ zOffset = 0
914
+ if placement.lower() == "lowerleft":
915
+ xOffset = -minX
916
+ yOffset = -minY
917
+ zOffset = -minZ
918
+ elif placement.lower() == "bottom":
919
+ xOffset = -(minX + (maxX - minX)*0.5)
920
+ yOffset = -(minY + (maxY - minY)*0.5)
921
+ zOffset = -minZ
922
+ elif placement.lower() == "center":
923
+ xOffset = -(minX + (maxX - minX)*0.5)
924
+ yOffset = -(minY + (maxY - minY)*0.5)
925
+ zOffset = -(minZ + (maxZ - minZ)*0.5)
926
+ returnTopology = Topology.Translate(returnTopology, xOffset, yOffset, zOffset)
927
+ returnTopology = Topology.Place(returnTopology, originA=Vertex.Origin(), originB=origin)
928
+ returnTopology = Topology.Orient(returnTopology, origin=origin, dirA=[0, 0, 1], dirB=direction)
929
+ return returnTopology
930
+
931
+ @staticmethod
932
+ def InternalBoundaries(shell: topologic.Shell, tolerance=0.0001) -> topologic.Topology:
933
+ """
934
+ Returns the internal boundaries (closed wires) of the input shell. Internal boundaries are considered holes.
935
+
936
+ Parameters
937
+ ----------
938
+ shell : topologic.Shell
939
+ The input shell.
940
+ tolerance : float , optional
941
+ The desired tolerance. The default is 0.0001.
942
+
943
+ Returns
944
+ -------
945
+ list
946
+ The list of internal boundaries
947
+
948
+ """
949
+ from topologicpy.Cluster import Cluster
950
+ from topologicpy.Topology import Topology
951
+ edges = []
952
+ _ = shell.Edges(None, edges)
953
+ ibEdges = []
954
+ for anEdge in edges:
955
+ faces = []
956
+ _ = anEdge.Faces(shell, faces)
957
+ if len(faces) > 1:
958
+ ibEdges.append(anEdge)
959
+ returnTopology = Topology.SelfMerge(Cluster.ByTopologies(ibEdges), tolerance=tolerance)
960
+ wires = Topology.Wires(returnTopology)
961
+ return wires
962
+
963
+
964
+ @staticmethod
965
+ def IsClosed(shell: topologic.Shell) -> bool:
966
+ """
967
+ Returns True if the input shell is closed. Returns False otherwise.
968
+
969
+ Parameters
970
+ ----------
971
+ shell : topologic.Shell
972
+ The input shell.
973
+
974
+ Returns
975
+ -------
976
+ bool
977
+ True if the input shell is closed. False otherwise.
978
+
979
+ """
980
+ return shell.IsClosed()
981
+
982
+ @staticmethod
983
+ def Pie(origin: topologic.Vertex = None, radiusA: float = 0.5, radiusB: float = 0.0, sides: int = 32, rings: int = 1, fromAngle: float = 0.0, toAngle: float = 360.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
984
+ """
985
+ Creates a pie shape.
986
+
987
+ Parameters
988
+ ----------
989
+ origin : topologic.Vertex , optional
990
+ The location of the origin of the pie. The default is None which results in the pie being placed at (0, 0, 0).
991
+ radiusA : float , optional
992
+ The outer radius of the pie. The default is 0.5.
993
+ radiusB : float , optional
994
+ The inner radius of the pie. The default is 0.25.
995
+ sides : int , optional
996
+ The number of sides of the pie. The default is 32.
997
+ rings : int , optional
998
+ The number of rings of the pie. The default is 1.
999
+ fromAngle : float , optional
1000
+ The angle in degrees from which to start creating the arc of the pie. The default is 0.
1001
+ toAngle : float , optional
1002
+ The angle in degrees at which to end creating the arc of the pie. The default is 360.
1003
+ direction : list , optional
1004
+ The vector representing the up direction of the pie. The default is [0, 0, 1].
1005
+ placement : str , optional
1006
+ The description of the placement of the origin of the pie. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1007
+ tolerance : float , optional
1008
+ The desired tolerance. The default is 0.0001.
1009
+
1010
+ Returns
1011
+ -------
1012
+ topologic.Shell
1013
+ The created pie.
1014
+
1015
+ """
1016
+ from topologicpy.Vertex import Vertex
1017
+ from topologicpy.Face import Face
1018
+ from topologicpy.Topology import Topology
1019
+ if not origin:
1020
+ origin = Vertex.ByCoordinates(0, 0, 0)
1021
+ if not isinstance(origin, topologic.Vertex):
1022
+ return None
1023
+ if toAngle < fromAngle:
1024
+ toAngle += 360
1025
+ if abs(toAngle-fromAngle) < tolerance:
1026
+ return None
1027
+ fromAngle = math.radians(fromAngle)
1028
+ toAngle = math.radians(toAngle)
1029
+ angleRange = toAngle - fromAngle
1030
+ radiusA = abs(radiusA)
1031
+ radiusB = abs(radiusB)
1032
+ if radiusB > radiusA:
1033
+ temp = radiusA
1034
+ radiusA = radiusB
1035
+ radiusB = temp
1036
+ if abs(radiusA - radiusB) < tolerance or radiusA < tolerance:
1037
+ return None
1038
+ radiusRange = radiusA - radiusB
1039
+ sides = int(abs(math.floor(sides)))
1040
+ if sides < 3:
1041
+ return None
1042
+ rings = int(abs(rings))
1043
+ if radiusB < tolerance:
1044
+ radiusB = 0
1045
+ xOffset = 0
1046
+ yOffset = 0
1047
+ zOffset = 0
1048
+ if placement.lower() == "lowerleft":
1049
+ xOffset = radiusA
1050
+ yOffset = radiusA
1051
+ uOffset = float(angleRange)/float(sides)
1052
+ vOffset = float(radiusRange)/float(rings)
1053
+ faces = []
1054
+ if radiusB > tolerance:
1055
+ for i in range(rings):
1056
+ r1 = radiusA - vOffset*i
1057
+ r2 = radiusA - vOffset*(i+1)
1058
+ for j in range(sides):
1059
+ a1 = fromAngle + uOffset*j
1060
+ a2 = fromAngle + uOffset*(j+1)
1061
+ x1 = math.sin(a1)*r1
1062
+ y1 = math.cos(a1)*r1
1063
+ z1 = 0
1064
+ x2 = math.sin(a1)*r2
1065
+ y2 = math.cos(a1)*r2
1066
+ z2 = 0
1067
+ x3 = math.sin(a2)*r2
1068
+ y3 = math.cos(a2)*r2
1069
+ z3 = 0
1070
+ x4 = math.sin(a2)*r1
1071
+ y4 = math.cos(a2)*r1
1072
+ z4 = 0
1073
+ v1 = Vertex.ByCoordinates(x1,y1,z1)
1074
+ v2 = Vertex.ByCoordinates(x2,y2,z2)
1075
+ v3 = Vertex.ByCoordinates(x3,y3,z3)
1076
+ v4 = Vertex.ByCoordinates(x4,y4,z4)
1077
+ f1 = Face.ByVertices([v1,v2,v3,v4])
1078
+ faces.append(f1)
1079
+ else:
1080
+ x1 = 0
1081
+ y1 = 0
1082
+ z1 = 0
1083
+ v1 = Vertex.ByCoordinates(x1,y1,z1)
1084
+ for j in range(sides):
1085
+ a1 = fromAngle + uOffset*j
1086
+ a2 = fromAngle + uOffset*(j+1)
1087
+ x2 = math.sin(a1)*radiusA
1088
+ y2 = math.cos(a1)*radiusA
1089
+ z2 = 0
1090
+ x3 = math.sin(a2)*radiusA
1091
+ y3 = math.cos(a2)*radiusA
1092
+ z3 = 0
1093
+ v2 = Vertex.ByCoordinates(x2,y2,z2)
1094
+ v3 = Vertex.ByCoordinates(x3,y3,z3)
1095
+ f1 = Face.ByVertices([v2,v1,v3])
1096
+ faces.append(f1)
1097
+
1098
+ shell = Shell.ByFaces(faces, tolerance=tolerance)
1099
+ if not shell:
1100
+ return None
1101
+ shell = Topology.Translate(shell, xOffset, yOffset, zOffset)
1102
+ shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin)
1103
+ shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction)
1104
+ return shell
1105
+
1106
+
1107
+ @staticmethod
1108
+ def Planarize(shell: topologic.Shell, origin: topologic.Vertex = None, mantissa: int = 6, tolerance: float = 0.0001) -> topologic.Shell:
1109
+ """
1110
+ Returns a planarized version of the input shell.
1111
+
1112
+ Parameters
1113
+ ----------
1114
+ shell : topologic.Shell
1115
+ The input shell.
1116
+ tolerance : float, optional
1117
+ The desired tolerance. The default is 0.0001.
1118
+ origin : topologic.Vertex , optional
1119
+ The desired origin of the plane unto which the planar shell will be projected. If set to None, the centroid of the input shell will be chosen. The default is None.
1120
+ mantissa : int , optional
1121
+ The desired length of the mantissa. The default is 6.
1122
+
1123
+ Returns
1124
+ -------
1125
+ topologic.Shell
1126
+ The planarized shell.
1127
+
1128
+ """
1129
+ from topologicpy.Vertex import Vertex
1130
+ from topologicpy.Face import Face
1131
+ from topologicpy.Cluster import Cluster
1132
+ from topologicpy.Topology import Topology
1133
+
1134
+ if not isinstance(shell, topologic.Shell):
1135
+ print("Shell.Planarize - Error: The input wire parameter is not a valid topologic shell. Returning None.")
1136
+ return None
1137
+ if origin == None:
1138
+ origin = Vertex.Origin()
1139
+ if not isinstance(origin, topologic.Vertex):
1140
+ print("Shell.Planarize - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
1141
+ return None
1142
+
1143
+ vertices = Topology.Vertices(shell)
1144
+ faces = Topology.Faces(shell)
1145
+ plane_equation = Vertex.PlaneEquation(vertices, mantissa=mantissa)
1146
+ rect = Face.RectangleByPlaneEquation(origin=origin , equation=plane_equation, tolerance=tolerance)
1147
+ new_vertices = [Vertex.Project(v, rect, mantissa=mantissa) for v in vertices]
1148
+ new_shell = Topology.ReplaceVertices(shell, verticesA=vertices, verticesB=new_vertices)
1149
+ new_faces = Topology.Faces(new_shell)
1150
+ return Topology.SelfMerge(Cluster.ByTopologies(new_faces), tolerance=tolerance)
1151
+
1152
+ @staticmethod
1153
+ def Rectangle(origin: topologic.Vertex = None, width: float = 1.0, length: float = 1.0,
1154
+ uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1],
1155
+ placement: str = "center", tolerance: float = 0.0001) -> topologic.Shell:
1156
+ """
1157
+ Creates a rectangle.
1158
+
1159
+ Parameters
1160
+ ----------
1161
+ origin : topologic.Vertex , optional
1162
+ The location of the origin of the rectangle. The default is None which results in the rectangle being placed at (0, 0, 0).
1163
+ width : float , optional
1164
+ The width of the rectangle. The default is 1.0.
1165
+ length : float , optional
1166
+ The length of the rectangle. The default is 1.0.
1167
+ uSides : int , optional
1168
+ The number of sides along the width. The default is 2.
1169
+ vSides : int , optional
1170
+ The number of sides along the length. The default is 2.
1171
+ direction : list , optional
1172
+ The vector representing the up direction of the rectangle. The default is [0, 0, 1].
1173
+ placement : str , optional
1174
+ The description of the placement of the origin of the rectangle. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1175
+ tolerance : float , optional
1176
+ The desired tolerance. The default is 0.0001.
1177
+
1178
+ Returns
1179
+ -------
1180
+ topologic.Shell
1181
+ The created shell.
1182
+
1183
+ """
1184
+ from topologicpy.Vertex import Vertex
1185
+ from topologicpy.Wire import Wire
1186
+ from topologicpy.Face import Face
1187
+ if not origin:
1188
+ origin = Vertex.ByCoordinates(0, 0, 0)
1189
+ if not isinstance(origin, topologic.Vertex):
1190
+ return None
1191
+ uOffset = float(width)/float(uSides)
1192
+ vOffset = float(length)/float(vSides)
1193
+ faces = []
1194
+ if placement.lower() == "center":
1195
+ wOffset = width*0.5
1196
+ lOffset = length*0.5
1197
+ else:
1198
+ wOffset = 0
1199
+ lOffset = 0
1200
+ for i in range(uSides):
1201
+ for j in range(vSides):
1202
+ rOrigin = Vertex.ByCoordinates(i*uOffset - wOffset, j*vOffset - lOffset, 0)
1203
+ w = Wire.Rectangle(origin=rOrigin, width=uOffset, length=vOffset, direction=[0, 0, 1], placement="lowerleft", tolerance=tolerance)
1204
+ f = Face.ByWire(w, tolerance=tolerance)
1205
+ faces.append(f)
1206
+ shell = Shell.ByFaces(faces, tolerance=tolerance)
1207
+ shell = Topology.Place(shell, originA=Vertex.Origin(), originB=origin)
1208
+ shell = Topology.Orient(shell, origin=origin, dirA=[0, 0, 1], dirB=direction)
1209
+ return shell
1210
+
1211
+ @staticmethod
1212
+ def RemoveCollinearEdges(shell: topologic.Shell, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Wire:
1213
+ """
1214
+ Removes any collinear edges in the input shell.
1215
+
1216
+ Parameters
1217
+ ----------
1218
+ shell : topologic.Shell
1219
+ The input shell.
1220
+ angTolerance : float , optional
1221
+ The desired angular tolerance. The default is 0.1.
1222
+ tolerance : float , optional
1223
+ The desired tolerance. The default is 0.0001.
1224
+
1225
+ Returns
1226
+ -------
1227
+ topologic.Shell
1228
+ The created shell without any collinear edges.
1229
+
1230
+ """
1231
+ from topologicpy.Face import Face
1232
+
1233
+ if not isinstance(shell, topologic.Shell):
1234
+ print("Shell.RemoveCollinearEdges - Error: The input shell parameter is not a valid shell. Returning None.")
1235
+ return None
1236
+ faces = Shell.Faces(shell)
1237
+ clean_faces = []
1238
+ for face in faces:
1239
+ clean_faces.append(Face.RemoveCollinearEdges(face, angTolerance=angTolerance, tolerance=tolerance))
1240
+ return Shell.ByFaces(clean_faces, tolerance=tolerance)
1241
+
1242
+ @staticmethod
1243
+ def Roof(face, angle: float = 45, epsilon: float = 0.01, tolerance: float = 0.001):
1244
+ """
1245
+ Creates a hipped roof through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
1246
+ This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
1247
+
1248
+ Parameters
1249
+ ----------
1250
+ face : topologic.Face
1251
+ The input face.
1252
+ angle : float , optioal
1253
+ The desired angle in degrees of the roof. The default is 45.
1254
+ epsilon : float , optional
1255
+ The desired epsilon (another form of tolerance for distance from plane). The default is 0.01. (This is set to a larger number as it was found to work better)
1256
+ tolerance : float , optional
1257
+ The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
1258
+
1259
+ Returns
1260
+ -------
1261
+ topologic.Shell
1262
+ The created roof.
1263
+
1264
+ """
1265
+ from topologicpy.Vertex import Vertex
1266
+ from topologicpy.Wire import Wire
1267
+ from topologicpy.Face import Face
1268
+ from topologicpy.Shell import Shell
1269
+ from topologicpy.Cell import Cell
1270
+ from topologicpy.Cluster import Cluster
1271
+ from topologicpy.Topology import Topology
1272
+ from topologicpy.Dictionary import Dictionary
1273
+ import topologic_core as topologic
1274
+ import math
1275
+
1276
+ def nearest_vertex_2d(v, vertices, tolerance=0.001):
1277
+ for vertex in vertices:
1278
+ x2 = Vertex.X(vertex)
1279
+ y2 = Vertex.Y(vertex)
1280
+ temp_v = Vertex.ByCoordinates(x2, y2, Vertex.Z(v))
1281
+ if Vertex.Distance(v, temp_v) <= tolerance:
1282
+ return vertex
1283
+ return None
1284
+
1285
+ if not isinstance(face, topologic.Face):
1286
+ return None
1287
+ angle = abs(angle)
1288
+ if angle >= 90-tolerance:
1289
+ return None
1290
+ if angle < tolerance:
1291
+ return None
1292
+ origin = Topology.Centroid(face)
1293
+ normal = Face.Normal(face)
1294
+ flat_face = Topology.Flatten(face, origin=origin, direction=normal)
1295
+ roof = Wire.Roof(flat_face, angle=angle, tolerance=tolerance)
1296
+ if not roof:
1297
+ return None
1298
+ shell = Shell.Skeleton(flat_face, tolerance=tolerance)
1299
+ faces = Shell.Faces(shell)
1300
+ Topology.Show(shell)
1301
+ if not faces:
1302
+ return None
1303
+ triangles = []
1304
+ for face in faces:
1305
+ internalBoundaries = Face.InternalBoundaries(face)
1306
+ if len(internalBoundaries) == 0:
1307
+ if len(Topology.Vertices(face)) > 3:
1308
+ triangles += Face.Triangulate(face, tolerance=tolerance)
1309
+ else:
1310
+ triangles += [face]
1311
+
1312
+ roof_vertices = Topology.Vertices(roof)
1313
+ flat_vertices = []
1314
+ for rv in roof_vertices:
1315
+ flat_vertices.append(Vertex.ByCoordinates(Vertex.X(rv), Vertex.Y(rv), 0))
1316
+
1317
+ final_triangles = []
1318
+ for triangle in triangles:
1319
+ if len(Topology.Vertices(triangle)) > 3:
1320
+ triangles = Face.Triangulate(triangle, tolerance=tolerance)
1321
+ else:
1322
+ triangles = [triangle]
1323
+ final_triangles += triangles
1324
+
1325
+ final_faces = []
1326
+ for triangle in final_triangles:
1327
+ face_vertices = Topology.Vertices(triangle)
1328
+ top_vertices = []
1329
+ for sv in face_vertices:
1330
+ temp = nearest_vertex_2d(sv, roof_vertices, tolerance=tolerance)
1331
+ if temp:
1332
+ top_vertices.append(temp)
1333
+ else:
1334
+ top_vertices.append(sv)
1335
+ tri_face = Face.ByVertices(top_vertices)
1336
+ final_faces.append(tri_face)
1337
+
1338
+ shell = Shell.ByFaces(final_faces, tolerance=tolerance)
1339
+ if not shell:
1340
+ shell = Cluster.ByTopologies(final_faces)
1341
+ try:
1342
+ shell = Topology.RemoveCoplanarFaces(shell, epsilon=epsilon, tolerance=tolerance)
1343
+ except:
1344
+ pass
1345
+ return shell
1346
+
1347
+ @staticmethod
1348
+ def SelfMerge(shell: topologic.Shell, angTolerance: float = 0.1, tolerance: float = 0.0001) -> topologic.Face:
1349
+ """
1350
+ Creates a face by merging the faces of the input shell. The shell must be planar within the input angular tolerance.
1351
+
1352
+ Parameters
1353
+ ----------
1354
+ shell : topologic.Shell
1355
+ The input shell.
1356
+ angTolerance : float , optional
1357
+ The desired angular tolerance. The default is 0.1.
1358
+ tolerance : float , optional
1359
+ The desired tolerance. The default is 0.0001.
1360
+
1361
+ Returns
1362
+ -------
1363
+ topologic.Face
1364
+ The created face.
1365
+
1366
+ """
1367
+ from topologicpy.Wire import Wire
1368
+ from topologicpy.Face import Face
1369
+ from topologicpy.Shell import Shell
1370
+ from topologicpy.Topology import Topology
1371
+
1372
+ def planarizeList(wireList):
1373
+ returnList = []
1374
+ for aWire in wireList:
1375
+ returnList.append(Wire.Planarize(aWire))
1376
+ return returnList
1377
+ if not isinstance(shell, topologic.Shell):
1378
+ return None
1379
+ ext_boundary = Shell.ExternalBoundary(shell, tolerance=tolerance)
1380
+ if isinstance(ext_boundary, topologic.Wire):
1381
+ f = Face.ByWire(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance) or Face.ByWire(Wire.Planarize(Topology.RemoveCollinearEdges(ext_boundary, angTolerance), tolerance=tolerance))
1382
+ if not f:
1383
+ print("FaceByPlanarShell - Error: The input Wire is not planar and could not be fixed. Returning None.")
1384
+ return None
1385
+ else:
1386
+ return f
1387
+ elif isinstance(ext_boundary, topologic.Cluster):
1388
+ wires = []
1389
+ _ = ext_boundary.Wires(None, wires)
1390
+ faces = []
1391
+ areas = []
1392
+ for aWire in wires:
1393
+ try:
1394
+ aFace = topologic.Face.ByExternalBoundary(Topology.RemoveCollinearEdges(aWire, angTolerance))
1395
+ except:
1396
+ aFace = topologic.Face.ByExternalBoundary(Wire.Planarize(Topology.RemoveCollinearEdges(aWire, angTolerance)))
1397
+ anArea = Face.Area(aFace)
1398
+ faces.append(aFace)
1399
+ areas.append(anArea)
1400
+ max_index = areas.index(max(areas))
1401
+ ext_boundary = faces[max_index]
1402
+ int_boundaries = list(set(faces) - set([ext_boundary]))
1403
+ int_wires = []
1404
+ for int_boundary in int_boundaries:
1405
+ temp_wires = []
1406
+ _ = int_boundary.Wires(None, temp_wires)
1407
+ int_wires.append(Topology.RemoveCollinearEdges(temp_wires[0], angTolerance))
1408
+ temp_wires = []
1409
+ _ = ext_boundary.Wires(None, temp_wires)
1410
+ ext_wire = Topology.RemoveCollinearEdges(temp_wires[0], angTolerance)
1411
+ try:
1412
+ return Face.ByWires(ext_wire, int_wires, tolerance=tolerance)
1413
+ except:
1414
+ return Face.ByWires(Wire.Planarize(ext_wire), planarizeList(int_wires), tolerance=tolerance)
1415
+ else:
1416
+ return None
1417
+
1418
+ def Skeleton(face, tolerance: float = 0.001):
1419
+ """
1420
+ Creates a shell through a straight skeleton. This method is contributed by 高熙鹏 xipeng gao <gaoxipeng1998@gmail.com>
1421
+ This algorithm depends on the polyskel code which is included in the library. Polyskel code is found at: https://github.com/Botffy/polyskel
1422
+
1423
+ Parameters
1424
+ ----------
1425
+ face : topologic.Face
1426
+ The input face.
1427
+ tolerance : float , optional
1428
+ The desired tolerance. The default is 0.001. (This is set to a larger number as it was found to work better)
1429
+
1430
+ Returns
1431
+ -------
1432
+ topologic.Shell
1433
+ The created straight skeleton.
1434
+
1435
+ """
1436
+ from topologicpy.Wire import Wire
1437
+ from topologicpy.Face import Face
1438
+ from topologicpy.Topology import Topology
1439
+ import topologic_core as topologic
1440
+ import math
1441
+
1442
+ if not isinstance(face, topologic.Face):
1443
+ return None
1444
+ roof = Wire.Skeleton(face, tolerance=tolerance)
1445
+ if not roof:
1446
+ return None
1447
+ br = Wire.BoundingRectangle(roof) #This works even if it is a Cluster not a Wire
1448
+ br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1)
1449
+ bf = Face.ByWire(br, tolerance=tolerance)
1450
+ large_shell = Topology.Boolean(bf, roof, operation="slice", tolerance=tolerance)
1451
+ if not large_shell:
1452
+ return None
1453
+ faces = Topology.Faces(large_shell)
1454
+ if not faces:
1455
+ return None
1456
+ final_faces = []
1457
+ for f in faces:
1458
+ internalBoundaries = Face.InternalBoundaries(f)
1459
+ if len(internalBoundaries) == 0:
1460
+ final_faces.append(f)
1461
+ shell = Shell.ByFaces(final_faces, tolerance=tolerance)
1462
+ return shell
1463
+
1464
+ @staticmethod
1465
+ def Simplify(shell, simplifyBoundary=True, tolerance=0.0001):
1466
+ """
1467
+ Simplifies the input shell edges based on the Douglas Peucker algorthim. See https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
1468
+ Part of this code was contributed by gaoxipeng. See https://github.com/wassimj/topologicpy/issues/35
1469
+
1470
+ Parameters
1471
+ ----------
1472
+ shell : topologic.Shell
1473
+ The input shell.
1474
+ simplifyBoundary : bool , optional
1475
+ If set to True, the external boundary of the shell will be simplified as well. Otherwise, it will not be simplified. The default is True.
1476
+ tolerance : float , optional
1477
+ The desired tolerance. The default is 0.0001. Edges shorter than this length will be removed.
1478
+
1479
+ Returns
1480
+ -------
1481
+ topologic.Shell
1482
+ The simplified shell.
1483
+
1484
+ """
1485
+ from topologicpy.Vertex import Vertex
1486
+ from topologicpy.Wire import Wire
1487
+ from topologicpy.Face import Face
1488
+ from topologicpy.Shell import Shell
1489
+ from topologicpy.Cluster import Cluster
1490
+ from topologicpy.Topology import Topology
1491
+ from topologicpy.Helper import Helper
1492
+
1493
+ def perpendicular_distance(point, line_start, line_end):
1494
+ # Calculate the perpendicular distance from a point to a line segment
1495
+ x0 = point.X()
1496
+ y0 = point.Y()
1497
+ x1 = line_start.X()
1498
+ y1 = line_start.Y()
1499
+ x2 = line_end.X()
1500
+ y2 = line_end.Y()
1501
+
1502
+ numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1)
1503
+ denominator = Vertex.Distance(line_start, line_end)
1504
+
1505
+ return numerator / denominator
1506
+
1507
+ def douglas_peucker(wire, tolerance):
1508
+ if isinstance(wire, list):
1509
+ points = wire
1510
+ else:
1511
+ points = Wire.Vertices(wire)
1512
+ # points.insert(0, points.pop())
1513
+ if len(points) <= 2:
1514
+ return points
1515
+
1516
+ # Use the first and last points in the list as the starting and ending points
1517
+ start_point = points[0]
1518
+ end_point = points[-1]
1519
+
1520
+ # Find the point with the maximum distance
1521
+ max_distance = 0
1522
+ max_index = 0
1523
+
1524
+ for i in range(1, len(points) - 1):
1525
+ d = perpendicular_distance(points[i], start_point, end_point)
1526
+ if d > max_distance:
1527
+ max_distance = d
1528
+ max_index = i
1529
+
1530
+ # If the maximum distance is less than the tolerance, no further simplification is needed
1531
+ if max_distance <= tolerance:
1532
+ return [start_point, end_point]
1533
+
1534
+ # Recursively simplify
1535
+ first_segment = douglas_peucker(points[:max_index + 1], tolerance)
1536
+ second_segment = douglas_peucker(points[max_index:], tolerance)
1537
+
1538
+ # Merge the two simplified segments
1539
+ return first_segment[:-1] + second_segment
1540
+ if not isinstance(shell, topologic.Shell):
1541
+ print("Shell.Simplify - Error: The input shell parameter is not a valid topologic shell. Returning None.")
1542
+ return None
1543
+ # Get the external boundary of the shell. This can be simplified as well, but might cause issues at the end.
1544
+ # At this point, it is assumed to be left as is.
1545
+ all_edges = Topology.Edges(shell)
1546
+ if simplifyBoundary == False:
1547
+ ext_boundary = Face.ByWire(Shell.ExternalBoundary(shell, tolerance=tolerance), tolerance=tolerance)
1548
+
1549
+ # Get the internal edges of the shell.
1550
+ i_edges = []
1551
+ for edge in all_edges:
1552
+ faces = Topology.SuperTopologies(edge, shell, topologyType="face")
1553
+ if len(faces) > 1: # This means that the edge separates two faces so it is internal.
1554
+ i_edges.append(edge)
1555
+ # Creat a Wire from the internal edges
1556
+ wire = Topology.SelfMerge(Cluster.ByTopologies(i_edges), tolerance=tolerance)
1557
+ else:
1558
+ wire = Topology.SelfMerge(Cluster.ByTopologies(all_edges), tolerance=tolerance)
1559
+ # Split the wires at its junctions (where more than two edges meet at a vertex)
1560
+ components = Wire.Split(wire)
1561
+ separators = []
1562
+ wires = []
1563
+ for component in components:
1564
+ if isinstance(component, topologic.Cluster):
1565
+ component = Topology.SelfMerge(component, tolerance=tolerance)
1566
+ if isinstance(component, topologic.Cluster):
1567
+ separators.append(Cluster.FreeEdges(component, tolerance=tolerance))
1568
+ wires.append(Cluster.FreeWires(component, tolerance=tolerance))
1569
+ if isinstance(component, topologic.Edge):
1570
+ separators.append(component)
1571
+ if isinstance(component, topologic.Wire):
1572
+ wires.append(component)
1573
+ if isinstance(component, topologic.Edge):
1574
+ separators.append(component)
1575
+ if isinstance(component, topologic.Wire):
1576
+ wires.append(component)
1577
+ wires = Helper.Flatten(wires)
1578
+ separators = Helper.Flatten(separators)
1579
+ results = []
1580
+ for w in wires:
1581
+ temp_wire = Wire.ByVertices(douglas_peucker(w, tolerance), close=False)
1582
+ results.append(temp_wire)
1583
+ # Make a Cluster out of the results
1584
+ cluster = Cluster.ByTopologies(results)
1585
+ # Get all the edges of the result
1586
+ edges = Topology.Edges(cluster)
1587
+ # Add them to the final edges
1588
+ final_edges = edges + separators
1589
+ # Make a Cluster out of the final set of edges
1590
+ cluster = Cluster.ByTopologies(final_edges)
1591
+ if simplifyBoundary == False:
1592
+ # Slice the external boundary of the shell by the cluster
1593
+ final_result = Topology.Slice(ext_boundary, cluster, tolerance=tolerance)
1594
+ else:
1595
+ br = Wire.BoundingRectangle(shell)
1596
+ br = Topology.Scale(br, Topology.Centroid(br), 1.5, 1.5, 1.5)
1597
+ br = Face.ByWire(br, tolerance=tolerance)
1598
+ v = Face.VertexByParameters(br, 0.1, 0.1)
1599
+ result = Topology.Slice(br, cluster, tolerance=tolerance)
1600
+ faces = Topology.Faces(result)
1601
+ final_faces = []
1602
+ for face in faces:
1603
+ if not Vertex.IsInternal(v, face, tolerance=0.01):
1604
+ final_faces.append(face)
1605
+ final_result = Shell.ByFaces(final_faces, tolerance=tolerance)
1606
+ return final_result
1607
+
1608
+
1609
+ @staticmethod
1610
+ def Vertices(shell: topologic.Shell) -> list:
1611
+ """
1612
+ Returns the vertices of the input shell.
1613
+
1614
+ Parameters
1615
+ ----------
1616
+ shell : topologic.Shell
1617
+ The input shell.
1618
+
1619
+ Returns
1620
+ -------
1621
+ list
1622
+ The list of vertices.
1623
+
1624
+ """
1625
+ if not isinstance(shell, topologic.Shell):
1626
+ return None
1627
+ vertices = []
1628
+ _ = shell.Vertices(None, vertices)
1629
+ return vertices
1630
+
1631
+ @staticmethod
1632
+ def Voronoi(vertices: list, face: topologic.Face = None, tolerance: float = 0.0001) -> topologic.Shell:
1633
+ """
1634
+ Returns a voronoi partitioning of the input face based on the input vertices. The vertices must be coplanar and within the face. See https://en.wikipedia.org/wiki/Voronoi_diagram.
1635
+
1636
+ Parameters
1637
+ ----------
1638
+ vertices : list
1639
+ The input list of vertices.
1640
+ face : topologic.Face , optional
1641
+ The input face. If the face is not set an optimised bounding rectangle of the input vertices is used instead. The default is None.
1642
+ tolerance : float , optional
1643
+ The desired tolerance. The default is 0.0001.
1644
+
1645
+ Returns
1646
+ -------
1647
+ shell
1648
+ A shell representing the voronoi partitioning of the input face.
1649
+
1650
+ """
1651
+ from topologicpy.Vertex import Vertex
1652
+ from topologicpy.Edge import Edge
1653
+ from topologicpy.Wire import Wire
1654
+ from topologicpy.Face import Face
1655
+ from topologicpy.Cluster import Cluster
1656
+ from topologicpy.Topology import Topology
1657
+ from topologicpy.Dictionary import Dictionary
1658
+
1659
+ if not isinstance(face, topologic.Face):
1660
+ cluster = Cluster.ByTopologies(vertices)
1661
+ br = Wire.BoundingRectangle(cluster, optimize=5)
1662
+ face = Face.ByWire(br, tolerance=tolerance)
1663
+ if not isinstance(vertices, list):
1664
+ return None
1665
+ vertices = [x for x in vertices if isinstance(x, topologic.Vertex)]
1666
+ if len(vertices) < 2:
1667
+ return None
1668
+
1669
+ # Flatten the input face
1670
+ origin = Topology.Centroid(face)
1671
+ normal = Face.Normal(face)
1672
+ flatFace = Topology.Flatten(face, origin=origin, direction=normal)
1673
+ eb = Face.ExternalBoundary(flatFace)
1674
+ ibList = Face.InternalBoundaries(flatFace)
1675
+ temp_verts = Topology.Vertices(eb)
1676
+ new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts]
1677
+ eb = Wire.ByVertices(new_verts, close=True)
1678
+ new_ibList = []
1679
+ for ib in ibList:
1680
+ temp_verts = Topology.Vertices(ib)
1681
+ new_verts = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in temp_verts]
1682
+ new_ibList.append(Wire.ByVertices(new_verts, close=True))
1683
+ flatFace = Face.ByWires(eb, new_ibList)
1684
+
1685
+ # Create a cluster of the input vertices
1686
+ verticesCluster = Cluster.ByTopologies(vertices)
1687
+
1688
+ # Flatten the cluster using the same transformations
1689
+ verticesCluster = Topology.Flatten(verticesCluster, origin=origin, direction=normal)
1690
+ flatVertices = Topology.Vertices(verticesCluster)
1691
+ flatVertices = [Vertex.ByCoordinates(Vertex.X(v), Vertex.Y(v), 0) for v in flatVertices]
1692
+ points = []
1693
+ for flatVertex in flatVertices:
1694
+ points.append([flatVertex.X(), flatVertex.Y()])
1695
+
1696
+ br = Wire.BoundingRectangle(flatFace)
1697
+ br_vertices = Wire.Vertices(br)
1698
+ br_x = []
1699
+ br_y = []
1700
+ for br_v in br_vertices:
1701
+ x, y = Vertex.Coordinates(br_v, outputType="xy")
1702
+ br_x.append(x)
1703
+ br_y.append(y)
1704
+ min_x = min(br_x)
1705
+ max_x = max(br_x)
1706
+ min_y = min(br_y)
1707
+ max_y = max(br_y)
1708
+ br_width = abs(max_x - min_x)
1709
+ br_length = abs(max_y - min_y)
1710
+
1711
+ points.append((-br_width*4, -br_length*4))
1712
+ points.append((-br_width*4, br_length*4))
1713
+ points.append((br_width*4, -br_length*4))
1714
+ points.append((br_width*4, br_length*4))
1715
+
1716
+ voronoi = Voronoi(points, furthest_site=False)
1717
+ voronoiVertices = []
1718
+ for v in voronoi.vertices:
1719
+ voronoiVertices.append(Vertex.ByCoordinates(v[0], v[1], 0))
1720
+
1721
+ faces = []
1722
+ for region in voronoi.regions:
1723
+ tempWire = []
1724
+ if len(region) > 1 and not -1 in region:
1725
+ for v in region:
1726
+ tempWire.append(Vertex.ByCoordinates(voronoiVertices[v].X(), voronoiVertices[v].Y(), 0))
1727
+ temp_verts = []
1728
+ for v in tempWire:
1729
+ if len(temp_verts) == 0:
1730
+ temp_verts.append(v)
1731
+ elif Vertex.Index(v, temp_verts) == None:
1732
+ temp_verts.append(v)
1733
+ tempWire = temp_verts
1734
+ temp_w = Wire.ByVertices(tempWire, close=True)
1735
+ faces.append(Face.ByWire(Wire.ByVertices(tempWire, close=True), tolerance=tolerance))
1736
+ shell = Shell.ByFaces(faces, tolerance=tolerance)
1737
+ edges = Shell.Edges(shell)
1738
+ edgesCluster = Cluster.ByTopologies(edges)
1739
+ shell = Topology.Slice(flatFace,edgesCluster, tolerance=tolerance)
1740
+ shell = Topology.Unflatten(shell, origin=origin, direction=normal)
1741
+ return shell
1742
+
1743
+ @staticmethod
1744
+ def Wires(shell: topologic.Shell) -> list:
1745
+ """
1746
+ Returns the wires of the input shell.
1747
+
1748
+ Parameters
1749
+ ----------
1750
+ shell : topologic.Shell
1751
+ The input shell.
1752
+
1753
+ Returns
1754
+ -------
1755
+ list
1756
+ The list of wires.
1757
+
1758
+ """
1759
+ if not isinstance(shell, topologic.Shell):
1760
+ return None
1761
+ wires = []
1762
+ _ = shell.Wires(None, wires)
1763
+ return wires
1764
+
1765
+
1766
+
1767
+
1768
+
1769
1769