topologicpy 0.8.26__py3-none-any.whl → 0.8.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- topologicpy/Face.py +38 -6
- topologicpy/Graph.py +198 -0
- topologicpy/Grid.py +16 -12
- topologicpy/Plotly.py +3 -2
- topologicpy/Shell.py +36 -0
- topologicpy/Topology.py +301 -249
- topologicpy/Vertex.py +95 -0
- topologicpy/version.py +1 -1
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.28.dist-info}/METADATA +1 -1
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.28.dist-info}/RECORD +13 -13
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.28.dist-info}/WHEEL +1 -1
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.28.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.28.dist-info}/top_level.txt +0 -0
topologicpy/Face.py
CHANGED
@@ -1927,6 +1927,34 @@ class Face():
|
|
1927
1927
|
else:
|
1928
1928
|
inverted_face = Face.ByWires(inverted_wire, internal_boundaries, tolerance=tolerance)
|
1929
1929
|
return inverted_face
|
1930
|
+
@staticmethod
|
1931
|
+
def IsConvex(face, mantissa: int = 6, silent: bool = False) -> bool:
|
1932
|
+
"""
|
1933
|
+
Returns True if the input face is convex. Returns False otherwise.
|
1934
|
+
|
1935
|
+
Parameters
|
1936
|
+
----------
|
1937
|
+
face : topologic_core.Face
|
1938
|
+
The input face.
|
1939
|
+
mantissa : int , optional
|
1940
|
+
The length of the desired mantissa. The default is 6.
|
1941
|
+
silent : bool , optional
|
1942
|
+
If set to True no warnings or errors are printed. The default is False.
|
1943
|
+
Returns
|
1944
|
+
-------
|
1945
|
+
bool
|
1946
|
+
True if the nput face is convex. False otherwise.
|
1947
|
+
|
1948
|
+
"""
|
1949
|
+
from topologicpy.Topology import Topology
|
1950
|
+
|
1951
|
+
if not Topology.IsInstance(face, "face"):
|
1952
|
+
if not silent:
|
1953
|
+
print("Face.IsConvex - Error: The input face parameter is not a valid topologic face. Returning None.")
|
1954
|
+
return None
|
1955
|
+
eb = Face.ExternalBoundary(face)
|
1956
|
+
eb = Face.ByWire(eb)
|
1957
|
+
return all(Face.InteriorAngles(eb)) < 180
|
1930
1958
|
|
1931
1959
|
@staticmethod
|
1932
1960
|
def IsCoplanar(faceA, faceB, mantissa: int = 6, tolerance: float = 0.0001) -> bool:
|
@@ -2468,9 +2496,9 @@ class Face():
|
|
2468
2496
|
flat_face = Topology.Difference(flat_face, Face.ByWire(obs))
|
2469
2497
|
|
2470
2498
|
# Check that the viewpoint is inside the face
|
2471
|
-
if not Vertex.IsInternal(flat_vertex, flat_face):
|
2472
|
-
|
2473
|
-
|
2499
|
+
# if not Vertex.IsInternal(flat_vertex, flat_face):
|
2500
|
+
# print("Face.Isovist - Error: The viewpoint is not inside the face. Returning None.")
|
2501
|
+
# return None
|
2474
2502
|
targets = Topology.Vertices(flat_face)
|
2475
2503
|
distances = []
|
2476
2504
|
for target in targets:
|
@@ -3544,7 +3572,7 @@ class Face():
|
|
3544
3572
|
return Face.ByWire(wire, tolerance=tolerance)
|
3545
3573
|
|
3546
3574
|
@staticmethod
|
3547
|
-
def Triangulate(face, mode: int = 0, meshSize: float = None, mantissa: int = 6, tolerance: float = 0.0001) -> list:
|
3575
|
+
def Triangulate(face, mode: int = 0, meshSize: float = None, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False) -> list:
|
3548
3576
|
"""
|
3549
3577
|
Triangulates the input face and returns a list of faces.
|
3550
3578
|
|
@@ -3571,6 +3599,8 @@ class Face():
|
|
3571
3599
|
The desired length of the mantissa. The default is 6.
|
3572
3600
|
tolerance : float , optional
|
3573
3601
|
The desired tolerance. The default is 0.0001.
|
3602
|
+
silent : bool , optional
|
3603
|
+
If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
|
3574
3604
|
|
3575
3605
|
Returns
|
3576
3606
|
-------
|
@@ -3638,7 +3668,8 @@ class Face():
|
|
3638
3668
|
from topologicpy.Topology import Topology
|
3639
3669
|
|
3640
3670
|
if not Topology.IsInstance(face, "Face"):
|
3641
|
-
|
3671
|
+
if not silent:
|
3672
|
+
print("Face.Triangulate - Error: The input face parameter is not a valid face. Returning None.")
|
3642
3673
|
return None
|
3643
3674
|
if not meshSize:
|
3644
3675
|
bounding_face = Face.BoundingRectangle(face)
|
@@ -3713,7 +3744,8 @@ class Face():
|
|
3713
3744
|
return faces
|
3714
3745
|
|
3715
3746
|
if not Topology.IsInstance(face, "Face"):
|
3716
|
-
|
3747
|
+
if not silent:
|
3748
|
+
print("Face.Triangulate - Error: The input face parameter is not a valid face. Returning None.")
|
3717
3749
|
return None
|
3718
3750
|
vertices = Topology.Vertices(face)
|
3719
3751
|
if len(vertices) == 3: # Already a triangle
|
topologicpy/Graph.py
CHANGED
@@ -2455,6 +2455,204 @@ class Graph:
|
|
2455
2455
|
|
2456
2456
|
@staticmethod
|
2457
2457
|
def ByIFCFile(file,
|
2458
|
+
includeTypes: list = [],
|
2459
|
+
excludeTypes: list = [],
|
2460
|
+
includeRels: list = [],
|
2461
|
+
excludeRels: list = [],
|
2462
|
+
transferDictionaries: bool = False,
|
2463
|
+
useInternalVertex: bool = False,
|
2464
|
+
storeBREP: bool = False,
|
2465
|
+
removeCoplanarFaces: bool = False,
|
2466
|
+
xMin: float = -0.5, yMin: float = -0.5, zMin: float = -0.5,
|
2467
|
+
xMax: float = 0.5, yMax: float = 0.5, zMax: float = 0.5,
|
2468
|
+
epsilon: float = 0.0001,
|
2469
|
+
tolerance: float = 0.0001,
|
2470
|
+
silent: bool = False):
|
2471
|
+
|
2472
|
+
"""
|
2473
|
+
Create a Graph from an IFC file. This code is partially based on code from Bruno Postle.
|
2474
|
+
|
2475
|
+
Parameters
|
2476
|
+
----------
|
2477
|
+
file : file
|
2478
|
+
The input IFC file
|
2479
|
+
includeTypes : list , optional
|
2480
|
+
A list of IFC object types to include in the graph. The default is [] which means all object types are included.
|
2481
|
+
excludeTypes : list , optional
|
2482
|
+
A list of IFC object types to exclude from the graph. The default is [] which mean no object type is excluded.
|
2483
|
+
includeRels : list , optional
|
2484
|
+
A list of IFC relationship types to include in the graph. The default is [] which means all relationship types are included.
|
2485
|
+
excludeRels : list , optional
|
2486
|
+
A list of IFC relationship types to exclude from the graph. The default is [] which mean no relationship type is excluded.
|
2487
|
+
transferDictionaries : bool , optional
|
2488
|
+
NOT USED. If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. The default is False.
|
2489
|
+
useInternalVertex : bool , optional
|
2490
|
+
If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. The default is False.
|
2491
|
+
storeBREP : bool , optional
|
2492
|
+
If set to True, store the BRep of the subtopology in its representative vertex. The default is False.
|
2493
|
+
removeCoplanarFaces : bool , optional
|
2494
|
+
If set to True, coplanar faces are removed. Otherwise they are not. The default is False.
|
2495
|
+
xMin : float, optional
|
2496
|
+
The desired minimum value to assign for a vertex's X coordinate. The default is -0.5.
|
2497
|
+
yMin : float, optional
|
2498
|
+
The desired minimum value to assign for a vertex's Y coordinate. The default is -0.5.
|
2499
|
+
zMin : float, optional
|
2500
|
+
The desired minimum value to assign for a vertex's Z coordinate. The default is -0.5.
|
2501
|
+
xMax : float, optional
|
2502
|
+
The desired maximum value to assign for a vertex's X coordinate. The default is 0.5.
|
2503
|
+
yMax : float, optional
|
2504
|
+
The desired maximum value to assign for a vertex's Y coordinate. The default is 0.5.
|
2505
|
+
zMax : float, optional
|
2506
|
+
The desired maximum value to assign for a vertex's Z coordinate. The default is 0.5.
|
2507
|
+
tolerance : float , optional
|
2508
|
+
The desired tolerance. The default is 0.0001.
|
2509
|
+
|
2510
|
+
Returns
|
2511
|
+
-------
|
2512
|
+
topologic_core.Graph
|
2513
|
+
The created graph.
|
2514
|
+
|
2515
|
+
"""
|
2516
|
+
|
2517
|
+
from topologicpy.Vertex import Vertex
|
2518
|
+
from topologicpy.Edge import Edge
|
2519
|
+
from topologicpy.Dictionary import Dictionary
|
2520
|
+
from topologicpy.Topology import Topology
|
2521
|
+
|
2522
|
+
def vertex_at_key_value(vertices, key, value):
|
2523
|
+
for v in vertices:
|
2524
|
+
d = Topology.Dictionary(v)
|
2525
|
+
d_value = Dictionary.ValueAtKey(d, key)
|
2526
|
+
if value == d_value:
|
2527
|
+
return v
|
2528
|
+
return None
|
2529
|
+
|
2530
|
+
def get_vertices(includeTypes=[], excludeTypes=[], removeCoplanarFaces=False, storeBREP=False, useInternalVertex=useInternalVertex, epsilon=0.0001, tolerance=0.0001):
|
2531
|
+
# Get the topologies
|
2532
|
+
topologies = Topology.ByIFCFile(file,
|
2533
|
+
includeTypes=includeTypes,
|
2534
|
+
excludeTypes=excludeTypes,
|
2535
|
+
transferDictionaries=True,
|
2536
|
+
removeCoplanarFaces=removeCoplanarFaces,
|
2537
|
+
epsilon=epsilon,
|
2538
|
+
tolerance=tolerance)
|
2539
|
+
vertices = []
|
2540
|
+
for topology in topologies:
|
2541
|
+
if Topology.IsInstance(topology, "Topology"):
|
2542
|
+
if useInternalVertex == True:
|
2543
|
+
v = Topology.InternalVertex(topology)
|
2544
|
+
else:
|
2545
|
+
v = Topology.Centroid(topology)
|
2546
|
+
d = Topology.Dictionary(topology)
|
2547
|
+
if storeBREP:
|
2548
|
+
d = Dictionary.SetValueAtKey(d, "BREP", Topology.BREPString(topology))
|
2549
|
+
if Topology.IsInstance(v, "vertex"):
|
2550
|
+
v = Topology.SetDictionary(v, Topology.Dictionary(topology))
|
2551
|
+
vertices.append(v)
|
2552
|
+
else:
|
2553
|
+
if not silent:
|
2554
|
+
ifc_id = Dictionary.ValueAtKey(Topology.Dictionary(topology), "IFC_global_id", 0)
|
2555
|
+
print(f"Graph.ByIFCFile - Warning: Could not create a vertex for entity {ifc_id}. Skipping")
|
2556
|
+
return vertices
|
2557
|
+
|
2558
|
+
# Get the relationships
|
2559
|
+
def get_relationships(ifc_file, includeRels=[], excludeRels=[]):
|
2560
|
+
include_set = set(s.lower() for s in includeRels)
|
2561
|
+
exclude_set = set(s.lower() for s in excludeRels)
|
2562
|
+
|
2563
|
+
relationships = [
|
2564
|
+
rel for rel in ifc_file.by_type("IfcRelationship")
|
2565
|
+
if (rel.is_a().lower() not in exclude_set) and
|
2566
|
+
(not include_set or rel.is_a().lower() in include_set)
|
2567
|
+
]
|
2568
|
+
|
2569
|
+
return relationships
|
2570
|
+
def get_edges(ifc_relationships, vertices):
|
2571
|
+
tuples = []
|
2572
|
+
edges = []
|
2573
|
+
|
2574
|
+
for ifc_rel in ifc_relationships:
|
2575
|
+
source = None
|
2576
|
+
destinations = []
|
2577
|
+
if ifc_rel.is_a("IfcRelConnectsPorts"):
|
2578
|
+
source = ifc_rel.RelatingPort
|
2579
|
+
destinations = ifc_rel.RelatedPorts
|
2580
|
+
elif ifc_rel.is_a("IfcRelConnectsPortToElement"):
|
2581
|
+
source = ifc_rel.RelatingPort
|
2582
|
+
destinations = [ifc_rel.RelatedElement]
|
2583
|
+
elif ifc_rel.is_a("IfcRelAggregates"):
|
2584
|
+
source = ifc_rel.RelatingObject
|
2585
|
+
destinations = ifc_rel.RelatedObjects
|
2586
|
+
elif ifc_rel.is_a("IfcRelNests"):
|
2587
|
+
source = ifc_rel.RelatingObject
|
2588
|
+
destinations = ifc_rel.RelatedObjects
|
2589
|
+
elif ifc_rel.is_a("IfcRelAssignsToGroup"):
|
2590
|
+
source = ifc_rel.RelatingGroup
|
2591
|
+
destinations = ifc_rel.RelatedObjects
|
2592
|
+
elif ifc_rel.is_a("IfcRelConnectsPathElements"):
|
2593
|
+
source = ifc_rel.RelatingElement
|
2594
|
+
destinations = [ifc_rel.RelatedElement]
|
2595
|
+
elif ifc_rel.is_a("IfcRelConnectsStructuralMember"):
|
2596
|
+
source = ifc_rel.RelatingStructuralMember
|
2597
|
+
destinations = [ifc_rel.RelatedStructuralConnection]
|
2598
|
+
elif ifc_rel.is_a("IfcRelContainedInSpatialStructure"):
|
2599
|
+
source = ifc_rel.RelatingStructure
|
2600
|
+
destinations = ifc_rel.RelatedElements
|
2601
|
+
elif ifc_rel.is_a("IfcRelFillsElement"):
|
2602
|
+
source = ifc_rel.RelatingOpeningElement
|
2603
|
+
destinations = [ifc_rel.RelatedBuildingElement]
|
2604
|
+
elif ifc_rel.is_a("IfcRelSpaceBoundary"):
|
2605
|
+
source = ifc_rel.RelatingSpace
|
2606
|
+
destinations = [ifc_rel.RelatedBuildingElement]
|
2607
|
+
elif ifc_rel.is_a("IfcRelVoidsElement"):
|
2608
|
+
source = ifc_rel.RelatingBuildingElement
|
2609
|
+
destinations = [ifc_rel.RelatedOpeningElement]
|
2610
|
+
elif ifc_rel.is_a("IfcRelDefinesByProperties") or ifc_rel.is_a("IfcRelAssociatesMaterial") or ifc_rel.is_a("IfcRelDefinesByType"):
|
2611
|
+
source = None
|
2612
|
+
destinations = None
|
2613
|
+
else:
|
2614
|
+
print("Graph.ByIFCFile - Warning: The relationship", ifc_rel, "is not supported. Skipping.")
|
2615
|
+
if source:
|
2616
|
+
sv = vertex_at_key_value(vertices, key="IFC_global_id", value=getattr(source, 'GlobalId', 0))
|
2617
|
+
if sv:
|
2618
|
+
si = Vertex.Index(sv, vertices, tolerance=tolerance)
|
2619
|
+
if not si == None:
|
2620
|
+
for destination in destinations:
|
2621
|
+
if destination == None:
|
2622
|
+
continue
|
2623
|
+
ev = vertex_at_key_value(vertices, key="IFC_global_id", value=getattr(destination, 'GlobalId', 0))
|
2624
|
+
if ev:
|
2625
|
+
ei = Vertex.Index(ev, vertices, tolerance=tolerance)
|
2626
|
+
if not ei == None:
|
2627
|
+
if not si == ei:
|
2628
|
+
if not [si,ei] in tuples:
|
2629
|
+
tuples.append([si,ei])
|
2630
|
+
tuples.append([ei,si])
|
2631
|
+
e = Edge.ByVertices([sv,ev])
|
2632
|
+
if Topology.IsInstance(e, "edge"):
|
2633
|
+
d = Dictionary.ByKeysValues(["IFC_global_id", "IFC_name", "IFC_type"], [ifc_rel.id(), ifc_rel.Name, ifc_rel.is_a()])
|
2634
|
+
e = Topology.SetDictionary(e, d)
|
2635
|
+
edges.append(e)
|
2636
|
+
else:
|
2637
|
+
if not silent:
|
2638
|
+
if not silent:
|
2639
|
+
print(f"Graph.ByIFCFile - Warning: Could not create an edge for relationship {ifc_rel.id()}. Skipping")
|
2640
|
+
|
2641
|
+
return edges
|
2642
|
+
|
2643
|
+
vertices = get_vertices(includeTypes=includeTypes,
|
2644
|
+
excludeTypes=excludeTypes,
|
2645
|
+
removeCoplanarFaces=removeCoplanarFaces,
|
2646
|
+
storeBREP=storeBREP,
|
2647
|
+
useInternalVertex=useInternalVertex,
|
2648
|
+
epsilon=epsilon,
|
2649
|
+
tolerance=0.0001)
|
2650
|
+
relationships = get_relationships(file, includeRels=includeRels, excludeRels=excludeRels)
|
2651
|
+
edges = get_edges(relationships, vertices)
|
2652
|
+
return Graph.ByVerticesEdges(vertices, edges)
|
2653
|
+
|
2654
|
+
@staticmethod
|
2655
|
+
def ByIFCFile_old(file,
|
2458
2656
|
includeTypes: list = [],
|
2459
2657
|
excludeTypes: list = [],
|
2460
2658
|
includeRels: list = [],
|
topologicpy/Grid.py
CHANGED
@@ -286,12 +286,11 @@ class Grid():
|
|
286
286
|
vTempVec = Vector.Multiply(uvVector, v, tolerance=tolerance)
|
287
287
|
gridVertex = Vertex.ByCoordinates(Vertex.X(origin, mantissa=mantissa)+uTempVec[0], Vertex.Y(origin, mantissa=mantissa)+vTempVec[1], Vertex.Z(origin, mantissa=mantissa)+uTempVec[2])
|
288
288
|
if clip and Topology.IsInstance(face, "Face"):
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
gridVertices.append(gridVertex)
|
289
|
+
if Vertex.IsInternal(gridVertex, face):
|
290
|
+
d = Dictionary.ByKeysValues(["u","v"],[u,v])
|
291
|
+
if d:
|
292
|
+
gridVertex.SetDictionary(d)
|
293
|
+
gridVertices.append(gridVertex)
|
295
294
|
grid = None
|
296
295
|
if len(gridVertices) > 0:
|
297
296
|
grid = Cluster.ByTopologies(gridVertices)
|
@@ -347,12 +346,17 @@ class Grid():
|
|
347
346
|
for v in vRange:
|
348
347
|
gridVertex = Face.VertexByParameters(face, u, v)
|
349
348
|
if clip and Topology.IsInstance(face, "Face"):
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
349
|
+
if Vertex.IsInternal(gridVertex, face):
|
350
|
+
d = Dictionary.ByKeysValues(["u","v"],[u,v])
|
351
|
+
if d:
|
352
|
+
gridVertex.SetDictionary(d)
|
353
|
+
gridVertices.append(gridVertex)
|
354
|
+
# gridVertex = gridVertex.Intersect(face, False)
|
355
|
+
# if Topology.IsInstance(gridVertex, "Vertex"):
|
356
|
+
# d = Dictionary.ByKeysValues(["u","v"],[u,v])
|
357
|
+
# if d:
|
358
|
+
# gridVertex.SetDictionary(d)
|
359
|
+
# gridVertices.append(gridVertex)
|
356
360
|
grid = None
|
357
361
|
if len(gridVertices) > 0:
|
358
362
|
grid = Cluster.ByTopologies(gridVertices)
|
topologicpy/Plotly.py
CHANGED
@@ -809,7 +809,8 @@ class Plotly:
|
|
809
809
|
faceLegendLabel="Topology Faces",
|
810
810
|
faceLegendRank=3,
|
811
811
|
faceLegendGroup=3,
|
812
|
-
intensityKey=None, intensities=[], colorScale="viridis",
|
812
|
+
intensityKey=None, intensities=[], colorScale="viridis",
|
813
|
+
mantissa=6, tolerance=0.0001, silent=False):
|
813
814
|
"""
|
814
815
|
Creates plotly face, edge, and vertex data.
|
815
816
|
|
@@ -1188,7 +1189,7 @@ class Plotly:
|
|
1188
1189
|
f_dictionaries = []
|
1189
1190
|
all_triangles = []
|
1190
1191
|
for tp_face in tp_faces:
|
1191
|
-
triangles = Face.Triangulate(tp_face, tolerance=tolerance)
|
1192
|
+
triangles = Face.Triangulate(tp_face, tolerance=tolerance, silent=silent)
|
1192
1193
|
if isinstance(triangles, list):
|
1193
1194
|
for tri in triangles:
|
1194
1195
|
if faceColorKey or faceOpacityKey or faceLabelKey or faceGroupKey:
|
topologicpy/Shell.py
CHANGED
@@ -1846,6 +1846,42 @@ class Shell():
|
|
1846
1846
|
final_result = Shell.ByFaces(final_faces, tolerance=tolerance)
|
1847
1847
|
return final_result
|
1848
1848
|
|
1849
|
+
@staticmethod
|
1850
|
+
def Square(origin= None, size: float = 1.0,
|
1851
|
+
uSides: int = 2, vSides: int = 2, direction: list = [0, 0, 1],
|
1852
|
+
placement: str = "center", tolerance: float = 0.0001):
|
1853
|
+
"""
|
1854
|
+
Creates a square.
|
1855
|
+
|
1856
|
+
Parameters
|
1857
|
+
----------
|
1858
|
+
origin : topologic_core.Vertex , optional
|
1859
|
+
The location of the origin of the square. The default is None which results in the square being placed at (0, 0, 0).
|
1860
|
+
size : float , optional
|
1861
|
+
The size of the square. The default is 1.0.
|
1862
|
+
length : float , optional
|
1863
|
+
The length of the square. The default is 1.0.
|
1864
|
+
uSides : int , optional
|
1865
|
+
The number of sides along the width. The default is 2.
|
1866
|
+
vSides : int , optional
|
1867
|
+
The number of sides along the length. The default is 2.
|
1868
|
+
direction : list , optional
|
1869
|
+
The vector representing the up direction of the square. The default is [0, 0, 1].
|
1870
|
+
placement : str , optional
|
1871
|
+
The description of the placement of the origin of the square. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
|
1872
|
+
tolerance : float , optional
|
1873
|
+
The desired tolerance. The default is 0.0001.
|
1874
|
+
|
1875
|
+
Returns
|
1876
|
+
-------
|
1877
|
+
topologic_core.Shell
|
1878
|
+
The created shell square.
|
1879
|
+
|
1880
|
+
"""
|
1881
|
+
return Shell.Rectangle(origin=origin, width=size, length=size,
|
1882
|
+
uSides=uSides, vSides=vSides, direction=direction,
|
1883
|
+
placement=placement, tolerance=tolerance)
|
1884
|
+
|
1849
1885
|
@staticmethod
|
1850
1886
|
def Vertices(shell) -> list:
|
1851
1887
|
"""
|
topologicpy/Topology.py
CHANGED
@@ -2118,9 +2118,13 @@ class Topology():
|
|
2118
2118
|
return Topology.ByDXFFile(file, sides=sides)
|
2119
2119
|
|
2120
2120
|
@staticmethod
|
2121
|
-
def ByIFCFile(file, includeTypes=[], excludeTypes=[], transferDictionaries=False,
|
2121
|
+
def ByIFCFile(file, includeTypes=[], excludeTypes=[], transferDictionaries=False,
|
2122
|
+
removeCoplanarFaces=False,
|
2123
|
+
xMin: float = -0.5, yMin: float = -0.5, zMin: float = -0.5,
|
2124
|
+
xMax: float = 0.5, yMax: float = 0.5, zMax: float = 0.5,
|
2125
|
+
epsilon=0.0001, tolerance=0.0001, silent=False):
|
2122
2126
|
"""
|
2123
|
-
Create a
|
2127
|
+
Create a topology by importing it from an IFC file.
|
2124
2128
|
|
2125
2129
|
Parameters
|
2126
2130
|
----------
|
@@ -2134,215 +2138,143 @@ class Topology():
|
|
2134
2138
|
If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. The default is False.
|
2135
2139
|
removeCoplanarFaces : bool , optional
|
2136
2140
|
If set to True, coplanar faces are removed. Otherwise they are not. The default is False.
|
2141
|
+
xMin : float, optional
|
2142
|
+
The desired minimum value to assign for a vertex's X coordinate. The default is -0.5.
|
2143
|
+
yMin : float, optional
|
2144
|
+
The desired minimum value to assign for a vertex's Y coordinate. The default is -0.5.
|
2145
|
+
zMin : float, optional
|
2146
|
+
The desired minimum value to assign for a vertex's Z coordinate. The default is -0.5.
|
2147
|
+
xMax : float, optional
|
2148
|
+
The desired maximum value to assign for a vertex's X coordinate. The default is 0.5.
|
2149
|
+
yMax : float, optional
|
2150
|
+
The desired maximum value to assign for a vertex's Y coordinate. The default is 0.5.
|
2151
|
+
zMax : float, optional
|
2152
|
+
The desired maximum value to assign for a vertex's Z coordinate. The default is 0.5.
|
2137
2153
|
epsilon : float , optional
|
2138
|
-
|
2139
|
-
|
2140
|
-
|
2154
|
+
The desired epsilon (another form of tolerance) for finding if two faces are coplanar. The default is 0.01.
|
2155
|
+
tolerance : float , optional
|
2156
|
+
The desired tolerance. The default is 0.0001.
|
2157
|
+
silent : bool , optional
|
2158
|
+
If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
|
2141
2159
|
Returns
|
2142
2160
|
-------
|
2143
2161
|
list
|
2144
2162
|
The created list of topologies.
|
2145
2163
|
|
2146
2164
|
"""
|
2147
|
-
|
2148
|
-
import ifcopenshell
|
2149
|
-
from topologicpy.Dictionary import Dictionary
|
2165
|
+
import ifcopenshell
|
2166
|
+
import ifcopenshell.geom
|
2150
2167
|
from topologicpy.Vertex import Vertex
|
2151
|
-
from topologicpy.
|
2168
|
+
from topologicpy.Wire import Wire
|
2152
2169
|
from topologicpy.Face import Face
|
2153
|
-
|
2170
|
+
from topologicpy.Cluster import Cluster
|
2171
|
+
from topologicpy.Topology import Topology
|
2172
|
+
from topologicpy.Dictionary import Dictionary
|
2173
|
+
from concurrent.futures import ThreadPoolExecutor
|
2174
|
+
import random
|
2154
2175
|
|
2155
|
-
|
2156
|
-
|
2157
|
-
|
2158
|
-
|
2159
|
-
|
2160
|
-
|
2161
|
-
raise ValueError("The provided entity does not have a GlobalId.")
|
2162
|
-
|
2163
|
-
# Get the property sets related to this entity
|
2164
|
-
for definition in entity.IsDefinedBy:
|
2165
|
-
if definition.is_a('IfcRelDefinesByProperties'):
|
2166
|
-
property_set = definition.RelatingPropertyDefinition
|
2167
|
-
|
2168
|
-
# Check if it is a property set
|
2169
|
-
if not property_set == None:
|
2170
|
-
if property_set.is_a('IfcPropertySet'):
|
2171
|
-
pset_name = "IFC_"+property_set.Name
|
2172
|
-
|
2173
|
-
# Dictionary to hold individual properties
|
2174
|
-
properties = {}
|
2175
|
-
|
2176
|
-
# Iterate over the properties in the PSET
|
2177
|
-
for prop in property_set.HasProperties:
|
2178
|
-
if prop.is_a('IfcPropertySingleValue'):
|
2179
|
-
# Get the property name and value
|
2180
|
-
prop_name = "IFC_"+prop.Name
|
2181
|
-
prop_value = prop.NominalValue.wrappedValue if prop.NominalValue else None
|
2182
|
-
properties[prop_name] = prop_value
|
2183
|
-
|
2184
|
-
# Add this PSET to the dictionary for this entity
|
2185
|
-
psets[pset_name] = properties
|
2186
|
-
return psets
|
2187
|
-
|
2188
|
-
def get_color_transparency_material(entity):
|
2189
|
-
import random
|
2176
|
+
# Early filtering in parallel (safe)
|
2177
|
+
def is_valid(product):
|
2178
|
+
is_a = product.is_a().lower()
|
2179
|
+
include = [t.lower() for t in includeTypes]
|
2180
|
+
exclude = [t.lower() for t in excludeTypes]
|
2181
|
+
return (not include or is_a in include) and (is_a not in exclude)
|
2190
2182
|
|
2191
|
-
|
2192
|
-
|
2193
|
-
|
2194
|
-
|
2195
|
-
|
2196
|
-
# Check if the entity is an opening or made of glass
|
2197
|
-
is_a = entity.is_a().lower()
|
2198
|
-
if "opening" in is_a or "window" in is_a or "door" in is_a or "space" in is_a:
|
2199
|
-
default_transparency = 0.7
|
2200
|
-
elif "space" in is_a:
|
2201
|
-
default_transparency = 0.8
|
2202
|
-
|
2203
|
-
# Check if the entity has constituent materials (e.g., glass)
|
2204
|
-
else:
|
2205
|
-
# Check for associated materials (ConstituentMaterial or direct material assignment)
|
2206
|
-
materials_checked = False
|
2207
|
-
if hasattr(entity, 'HasAssociations'):
|
2208
|
-
for rel in entity.HasAssociations:
|
2209
|
-
if rel.is_a('IfcRelAssociatesMaterial'):
|
2210
|
-
material = rel.RelatingMaterial
|
2211
|
-
if material.is_a('IfcMaterial') and 'glass' in material.Name.lower():
|
2212
|
-
default_transparency = 0.5
|
2213
|
-
materials_checked = True
|
2214
|
-
elif material.is_a('IfcMaterialLayerSetUsage'):
|
2215
|
-
material_layers = material.ForLayerSet.MaterialLayers
|
2216
|
-
for layer in material_layers:
|
2217
|
-
material_list.append(layer.Material.Name)
|
2218
|
-
if 'glass' in layer.Material.Name.lower():
|
2219
|
-
default_transparency = 0.5
|
2220
|
-
materials_checked = True
|
2221
|
-
|
2222
|
-
# Check for ConstituentMaterial if available
|
2223
|
-
if hasattr(entity, 'HasAssociations') and not materials_checked:
|
2224
|
-
for rel in entity.HasAssociations:
|
2225
|
-
if rel.is_a('IfcRelAssociatesMaterial'):
|
2226
|
-
material = rel.RelatingMaterial
|
2227
|
-
if material.is_a('IfcMaterialConstituentSet'):
|
2228
|
-
for constituent in material.MaterialConstituents:
|
2229
|
-
material_list.append(constituent.Material.Name)
|
2230
|
-
if 'glass' in constituent.Material.Name.lower():
|
2231
|
-
default_transparency = 0.5
|
2232
|
-
materials_checked = True
|
2233
|
-
|
2234
|
-
# Check if the entity has ShapeAspects with associated materials or styles
|
2235
|
-
if hasattr(entity, 'HasShapeAspects') and not materials_checked:
|
2236
|
-
for shape_aspect in entity.HasShapeAspects:
|
2237
|
-
if hasattr(shape_aspect, 'StyledByItem') and shape_aspect.StyledByItem:
|
2238
|
-
for styled_item in shape_aspect.StyledByItem:
|
2239
|
-
for style in styled_item.Styles:
|
2240
|
-
if style.is_a('IfcSurfaceStyle'):
|
2241
|
-
for surface_style in style.Styles:
|
2242
|
-
if surface_style.is_a('IfcSurfaceStyleRendering'):
|
2243
|
-
transparency = getattr(surface_style, 'Transparency', default_transparency)
|
2244
|
-
if transparency > 0:
|
2245
|
-
default_transparency = transparency
|
2246
|
-
|
2247
|
-
# Try to get the actual color and transparency if defined
|
2248
|
-
if hasattr(entity, 'Representation') and entity.Representation:
|
2249
|
-
for rep in entity.Representation.Representations:
|
2250
|
-
for item in rep.Items:
|
2251
|
-
if hasattr(item, 'StyledByItem') and item.StyledByItem:
|
2252
|
-
for styled_item in item.StyledByItem:
|
2253
|
-
if hasattr(styled_item, 'Styles'):
|
2254
|
-
for style in styled_item.Styles:
|
2255
|
-
if style.is_a('IfcSurfaceStyle'):
|
2256
|
-
for surface_style in style.Styles:
|
2257
|
-
if surface_style.is_a('IfcSurfaceStyleRendering'):
|
2258
|
-
color = surface_style.SurfaceColour
|
2259
|
-
transparency = getattr(surface_style, 'Transparency', default_transparency)
|
2260
|
-
return (color.Red*255, color.Green*255, color.Blue*255), transparency, material_list
|
2261
|
-
|
2262
|
-
# If no color is defined, return a consistent random color based on the entity type
|
2263
|
-
if "wall" in is_a:
|
2264
|
-
color = (175, 175, 175)
|
2265
|
-
elif "slab" in is_a:
|
2266
|
-
color = (200, 200, 200)
|
2267
|
-
elif "space" in is_a:
|
2268
|
-
color = (250, 250, 250)
|
2269
|
-
else:
|
2270
|
-
random.seed(hash(is_a))
|
2271
|
-
color = (random.random(), random.random(), random.random())
|
2272
|
-
|
2273
|
-
return color, default_transparency, material_list
|
2274
|
-
# Create a 4x4 unity matrix
|
2275
|
-
matrix = [[1.0 if i == j else 0.0 for j in range(4)] for i in range(4)]
|
2276
|
-
def convert_to_topology(entity, settings, transferDictionaries=False):
|
2277
|
-
if hasattr(entity, "Representation") and entity.Representation:
|
2278
|
-
for rep in entity.Representation.Representations:
|
2279
|
-
shape_topology = None
|
2280
|
-
if rep.is_a("IfcShapeRepresentation"):
|
2281
|
-
# Generate the geometry for this entity
|
2282
|
-
try:
|
2283
|
-
shape = ifcopenshell.geom.create_shape(settings, entity)
|
2284
|
-
trans = shape.transformation
|
2285
|
-
# Convert into a 4x4 matrix
|
2286
|
-
matrix = [trans.matrix[i:i+4] for i in range(0, len(trans.matrix), 4)]
|
2287
|
-
# Get grouped vertices and grouped faces
|
2288
|
-
grouped_verts = shape.geometry.verts
|
2289
|
-
verts = [ [grouped_verts[i], grouped_verts[i + 1], grouped_verts[i + 2]] for i in range(0, len(grouped_verts), 3)]
|
2290
|
-
grouped_edges = shape.geometry.edges
|
2291
|
-
edges = [[grouped_edges[i], grouped_edges[i + 1]] for i in range(0, len(grouped_edges), 2)]
|
2292
|
-
grouped_faces = shape.geometry.faces
|
2293
|
-
faces = [ [grouped_faces[i], grouped_faces[i + 1], grouped_faces[i + 2]] for i in range(0, len(grouped_faces), 3)]
|
2294
|
-
#shape_topology = ifc_to_topologic_geometry(verts, edges, faces)
|
2295
|
-
#shape_topology = Topology.SelfMerge(Topology.ByGeometry(verts, edges, faces))
|
2296
|
-
shape_topology = Topology.ByGeometry(verts, edges, faces, silent=True)
|
2297
|
-
if not shape_topology == None:
|
2298
|
-
if removeCoplanarFaces == True:
|
2299
|
-
shape_topology = Topology.RemoveCoplanarFaces(shape_topology, epsilon=0.0001)
|
2300
|
-
shape_topology = Topology.Transform(shape_topology, matrix)
|
2301
|
-
|
2302
|
-
# Store relevant information
|
2303
|
-
if transferDictionaries == True:
|
2304
|
-
color, transparency, material_list = get_color_transparency_material(entity)
|
2305
|
-
if color == None:
|
2306
|
-
color = "white"
|
2307
|
-
if transparency == None:
|
2308
|
-
transparency = 0
|
2309
|
-
entity_dict = {
|
2310
|
-
"TOPOLOGIC_id": str(Topology.UUID(shape_topology)),
|
2311
|
-
"TOPOLOGIC_name": getattr(entity, 'Name', "Untitled"),
|
2312
|
-
"TOPOLOGIC_type": Topology.TypeAsString(shape_topology),
|
2313
|
-
"TOPOLOGIC_color": color,
|
2314
|
-
"TOPOLOGIC_opacity": 1.0 - transparency,
|
2315
|
-
"IFC_global_id": getattr(entity, 'GlobalId', 0),
|
2316
|
-
"IFC_name": getattr(entity, 'Name', "Untitled"),
|
2317
|
-
"IFC_type": entity.is_a(),
|
2318
|
-
"IFC_material_list": material_list,
|
2319
|
-
}
|
2320
|
-
topology_dict = Dictionary.ByPythonDictionary(entity_dict)
|
2321
|
-
# Get PSETs dictionary
|
2322
|
-
pset_python_dict = get_psets(entity)
|
2323
|
-
pset_dict = Dictionary.ByPythonDictionary(pset_python_dict)
|
2324
|
-
topology_dict = Dictionary.ByMergedDictionaries([topology_dict, pset_dict])
|
2325
|
-
shape_topology = Topology.SetDictionary(shape_topology, topology_dict)
|
2326
|
-
except:
|
2327
|
-
pass
|
2328
|
-
return shape_topology
|
2329
|
-
return None
|
2183
|
+
with ThreadPoolExecutor() as executor:
|
2184
|
+
products = list(file.by_type("IfcProduct"))
|
2185
|
+
valid_entities = list(executor.map(lambda p: p if is_valid(p) else None, products))
|
2186
|
+
valid_entities = [e for e in valid_entities if e is not None]
|
2330
2187
|
|
2331
|
-
# Main Code
|
2332
|
-
topologies = []
|
2333
2188
|
settings = ifcopenshell.geom.settings()
|
2334
|
-
#settings.set("dimensionality", ifcopenshell.ifcopenshell_wrapper.SOLID)
|
2335
2189
|
settings.set(settings.USE_WORLD_COORDS, True)
|
2336
|
-
|
2337
|
-
|
2338
|
-
for product in products:
|
2339
|
-
is_a = product.is_a()
|
2340
|
-
if (is_a in includeTypes or len(includeTypes) == 0) and (not is_a in excludeTypes):
|
2341
|
-
entities.append(product)
|
2190
|
+
settings.set("iterator-output", ifcopenshell.ifcopenshell_wrapper.SERIALIZED)
|
2191
|
+
|
2342
2192
|
topologies = []
|
2343
|
-
for entity in
|
2344
|
-
|
2345
|
-
|
2193
|
+
for entity in valid_entities:
|
2194
|
+
if not hasattr(entity, "Representation") or not entity.Representation:
|
2195
|
+
continue
|
2196
|
+
shape = ifcopenshell.geom.create_shape(settings, entity)
|
2197
|
+
topology = None
|
2198
|
+
if hasattr(shape.geometry, 'brep_data'):
|
2199
|
+
brep_string = shape.geometry.brep_data
|
2200
|
+
topology = Topology.ByBREPString(brep_string)
|
2201
|
+
if not topology:
|
2202
|
+
if not silent:
|
2203
|
+
print(f"Topology.ByIFCFile - Warning: Could not convert entity {getattr(entity, 'GlobalId', 0)} to a topology. Skipping.")
|
2204
|
+
continue
|
2205
|
+
if removeCoplanarFaces:
|
2206
|
+
topology = Topology.RemoveCoplanarFaces(topology, epsilon=epsilon, tolerance=tolerance)
|
2207
|
+
else:
|
2208
|
+
if not silent:
|
2209
|
+
print(f"Topology.ByIFCFile - Warning: Entity {getattr(entity, 'GlobalId', 0)} does not have a BREP. Skipping.")
|
2210
|
+
continue
|
2211
|
+
|
2212
|
+
|
2213
|
+
if transferDictionaries:
|
2214
|
+
entity_dict = {
|
2215
|
+
"TOPOLOGIC_id": str(Topology.UUID(topology)),
|
2216
|
+
"TOPOLOGIC_name": getattr(entity, 'Name', "Untitled"),
|
2217
|
+
"TOPOLOGIC_type": Topology.TypeAsString(topology),
|
2218
|
+
"IFC_global_id": getattr(entity, 'GlobalId', 0),
|
2219
|
+
"IFC_name": getattr(entity, 'Name', "Untitled"),
|
2220
|
+
"IFC_type": entity.is_a()
|
2221
|
+
}
|
2222
|
+
|
2223
|
+
# Optionally add property sets
|
2224
|
+
psets = {}
|
2225
|
+
if hasattr(entity, 'IsDefinedBy'):
|
2226
|
+
for rel in entity.IsDefinedBy:
|
2227
|
+
if rel.is_a('IfcRelDefinesByProperties'):
|
2228
|
+
pdef = rel.RelatingPropertyDefinition
|
2229
|
+
if pdef and pdef.is_a('IfcPropertySet'):
|
2230
|
+
key = f"IFC_{pdef.Name}"
|
2231
|
+
props = {}
|
2232
|
+
for prop in pdef.HasProperties:
|
2233
|
+
if prop.is_a('IfcPropertySingleValue') and prop.NominalValue:
|
2234
|
+
props[f"IFC_{prop.Name}"] = prop.NominalValue.wrappedValue
|
2235
|
+
psets[key] = props
|
2236
|
+
|
2237
|
+
final_dict = Dictionary.ByPythonDictionary(entity_dict)
|
2238
|
+
if psets:
|
2239
|
+
pset_dict = Dictionary.ByPythonDictionary(psets)
|
2240
|
+
final_dict = Dictionary.ByMergedDictionaries([final_dict, pset_dict])
|
2241
|
+
|
2242
|
+
topology = Topology.SetDictionary(topology, final_dict)
|
2243
|
+
|
2244
|
+
topologies.append(topology)
|
2245
|
+
|
2246
|
+
final_topologies = []
|
2247
|
+
for topology in topologies:
|
2248
|
+
faces = []
|
2249
|
+
|
2250
|
+
for w in Topology.Wires(topology):
|
2251
|
+
# Skip trivial wires (e.g. edges)
|
2252
|
+
if len(Topology.Vertices(w)) < 3:
|
2253
|
+
continue
|
2254
|
+
|
2255
|
+
# Only attempt face creation if the wire is closed
|
2256
|
+
if Wire.IsClosed(w) and Wire.IsManifold(w):
|
2257
|
+
f = Face.ByWire(w)
|
2258
|
+
if f:
|
2259
|
+
faces.append(f)
|
2260
|
+
continue
|
2261
|
+
|
2262
|
+
# fallback: keep wire
|
2263
|
+
faces.append(w)
|
2264
|
+
|
2265
|
+
# Avoid unnecessary Cluster/SelfMerge if there's only one face
|
2266
|
+
if len(faces) == 1:
|
2267
|
+
final_topology = faces[0]
|
2268
|
+
else:
|
2269
|
+
final_topology = Topology.SelfMerge(Cluster.ByTopologies(faces))
|
2270
|
+
if final_topology == None:
|
2271
|
+
final_topology = Cluster.ByTopologies(faces)
|
2272
|
+
if transferDictionaries:
|
2273
|
+
d = Topology.Dictionary(topology)
|
2274
|
+
final_topology = Topology.SetDictionary(final_topology, d)
|
2275
|
+
|
2276
|
+
final_topologies.append(final_topology)
|
2277
|
+
return final_topologies
|
2346
2278
|
|
2347
2279
|
@staticmethod
|
2348
2280
|
def _ByIFCFile_old(file, includeTypes=[], excludeTypes=[], transferDictionaries=False, removeCoplanarFaces=False):
|
@@ -4398,75 +4330,137 @@ class Topology():
|
|
4398
4330
|
return contexts
|
4399
4331
|
|
4400
4332
|
@staticmethod
|
4401
|
-
def ConvexHull(topology, mantissa: int = 6, tolerance: float = 0.0001):
|
4333
|
+
def ConvexHull(topology, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
|
4402
4334
|
"""
|
4403
|
-
|
4335
|
+
Computes the convex hull of the input topology.
|
4336
|
+
Returns a Face in 2D (even embedded in 3D), or a Cell/Shell in 3D.
|
4404
4337
|
|
4405
4338
|
Parameters
|
4406
4339
|
----------
|
4407
4340
|
topology : topologic_core.Topology
|
4408
4341
|
The input Topology.
|
4409
|
-
mantissa : int
|
4410
|
-
|
4411
|
-
tolerance : float
|
4412
|
-
|
4342
|
+
mantissa : int, optional
|
4343
|
+
Precision of the coordinates. Default is 6.
|
4344
|
+
tolerance : float, optional
|
4345
|
+
Tolerance value for geometric operations. Default is 0.0001.
|
4346
|
+
silent : bool, optional
|
4347
|
+
If True, suppresses warning and error messages.
|
4413
4348
|
|
4414
4349
|
Returns
|
4415
4350
|
-------
|
4416
4351
|
topologic_core.Topology
|
4417
|
-
The
|
4418
|
-
|
4352
|
+
The convex hull of the topology as a Face (2D) or Cell/Shell (3D).
|
4419
4353
|
"""
|
4420
4354
|
from topologicpy.Vertex import Vertex
|
4421
4355
|
from topologicpy.Edge import Edge
|
4422
4356
|
from topologicpy.Wire import Wire
|
4423
4357
|
from topologicpy.Face import Face
|
4424
|
-
from topologicpy.Shell import Shell
|
4425
4358
|
from topologicpy.Cell import Cell
|
4426
|
-
from topologicpy.
|
4427
|
-
|
4428
|
-
|
4429
|
-
|
4430
|
-
|
4431
|
-
|
4432
|
-
|
4433
|
-
|
4434
|
-
|
4435
|
-
|
4436
|
-
|
4437
|
-
|
4438
|
-
|
4439
|
-
|
4440
|
-
|
4441
|
-
|
4442
|
-
|
4443
|
-
|
4444
|
-
|
4445
|
-
|
4446
|
-
|
4447
|
-
|
4448
|
-
|
4449
|
-
|
4450
|
-
|
4451
|
-
|
4452
|
-
|
4453
|
-
|
4359
|
+
from topologicpy.Shell import Shell
|
4360
|
+
from topologicpy.Cluster import Cluster
|
4361
|
+
from topologicpy.Topology import Topology
|
4362
|
+
|
4363
|
+
import numpy as np
|
4364
|
+
from scipy.spatial import ConvexHull as SciPyConvexHull
|
4365
|
+
|
4366
|
+
if not Topology.IsInstance(topology, "Topology"):
|
4367
|
+
if not silent:
|
4368
|
+
print("Topology.ConvexHull - Error: Input is not a valid topology. Returning None.")
|
4369
|
+
return None
|
4370
|
+
|
4371
|
+
def convexHull_2d(vertices, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
|
4372
|
+
from topologicpy.Vertex import Vertex
|
4373
|
+
from topologicpy.Edge import Edge
|
4374
|
+
from topologicpy.Wire import Wire
|
4375
|
+
from topologicpy.Face import Face
|
4376
|
+
from topologicpy.Cluster import Cluster
|
4377
|
+
from topologicpy.Topology import Topology
|
4378
|
+
|
4379
|
+
import numpy as np
|
4380
|
+
from scipy.spatial import ConvexHull as SciPyConvexHull
|
4381
|
+
|
4382
|
+
cluster = Cluster.ByTopologies(vertices)
|
4383
|
+
normal = Vertex.Normal(vertices)
|
4384
|
+
centroid = Topology.Centroid(cluster)
|
4385
|
+
flat_cluster = Topology.Flatten(cluster, origin=centroid, direction=normal)
|
4386
|
+
flat_verts = Topology.Vertices(flat_cluster)
|
4387
|
+
coords = np.array([[Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa)] for v in flat_verts])
|
4454
4388
|
try:
|
4455
|
-
|
4456
|
-
return c
|
4389
|
+
hull = SciPyConvexHull(coords)
|
4457
4390
|
except:
|
4458
|
-
|
4459
|
-
|
4460
|
-
|
4461
|
-
|
4462
|
-
|
4391
|
+
if not silent:
|
4392
|
+
print("Topology.ConvexHull - Error computing 2D hull. Returning None.")
|
4393
|
+
return None
|
4394
|
+
|
4395
|
+
# Reconstruct 3D points from 2D hull
|
4396
|
+
hull_indices = hull.vertices
|
4397
|
+
sorted_coords = coords[hull_indices]
|
4398
|
+
|
4399
|
+
edges = []
|
4400
|
+
for i in range(len(sorted_coords)):
|
4401
|
+
p1 = sorted_coords[i]
|
4402
|
+
p2 = sorted_coords[(i + 1) % len(sorted_coords)]
|
4403
|
+
v1 = Vertex.ByCoordinates(*p1)
|
4404
|
+
v2 = Vertex.ByCoordinates(*p2)
|
4405
|
+
edges.append(Edge.ByVertices([v1, v2], tolerance=tolerance))
|
4406
|
+
convex_hull = Wire.ByEdges(edges, tolerance=tolerance)
|
4407
|
+
if Topology.IsInstance(convex_hull, "wire"):
|
4408
|
+
convex_hull_2 = Face.ByWire(convex_hull, tolerance=tolerance, silent=True)
|
4409
|
+
if not Topology.IsInstance(convex_hull_2, "face"):
|
4410
|
+
convex_hull_2 = convex_hull
|
4411
|
+
convex_hull = Topology.Unflatten(convex_hull_2, origin=centroid, direction=normal)
|
4412
|
+
return convex_hull
|
4413
|
+
|
4414
|
+
vertices = Topology.Vertices(topology)
|
4415
|
+
if len(vertices) < 3:
|
4416
|
+
if not silent:
|
4417
|
+
print("Topology.ConvexHull - Error: Need at least 3 points to compute a convex hull.")
|
4463
4418
|
return None
|
4464
|
-
|
4465
|
-
|
4466
|
-
|
4467
|
-
|
4468
|
-
|
4469
|
-
|
4419
|
+
|
4420
|
+
coords = np.array([Vertex.Coordinates(v, mantissa=mantissa) for v in vertices])
|
4421
|
+
|
4422
|
+
if Vertex.AreCoplanar(vertices, mantissa=mantissa, tolerance=tolerance, silent=silent):
|
4423
|
+
# Planar case
|
4424
|
+
return convexHull_2d(vertices, mantissa=mantissa, tolerance=tolerance, silent=silent)
|
4425
|
+
else:
|
4426
|
+
# Non-planar case
|
4427
|
+
try:
|
4428
|
+
hull = SciPyConvexHull(coords)
|
4429
|
+
except:
|
4430
|
+
try:
|
4431
|
+
hull = SciPyConvexHull(coords, qhull_options="QJ")
|
4432
|
+
except Exception as e:
|
4433
|
+
if not silent:
|
4434
|
+
print(f"Topology.ConvexHull - 3D hull failed even with QJ: {e}")
|
4435
|
+
return None
|
4436
|
+
faces = []
|
4437
|
+
for simplex in hull.simplices:
|
4438
|
+
points = coords[simplex]
|
4439
|
+
edges = []
|
4440
|
+
for i in range(len(points)):
|
4441
|
+
p1 = points[i]
|
4442
|
+
p2 = points[(i + 1) % len(points)]
|
4443
|
+
v1 = Vertex.ByCoordinates(*p1)
|
4444
|
+
v2 = Vertex.ByCoordinates(*p2)
|
4445
|
+
edges.append(Edge.ByVertices([v1, v2], tolerance=tolerance))
|
4446
|
+
wire = Wire.ByEdges(edges, tolerance=tolerance)
|
4447
|
+
face = Face.ByWire(wire, tolerance=tolerance)
|
4448
|
+
faces.append(face)
|
4449
|
+
|
4450
|
+
convex_hull = Cell.ByFaces(faces, tolerance=tolerance, silent=True)
|
4451
|
+
if convex_hull:
|
4452
|
+
convex_hull_2 = Topology.RemoveCoplanarFaces(convex_hull, tolerance=tolerance, silent=True)
|
4453
|
+
if not convex_hull_2:
|
4454
|
+
return convex_hull
|
4455
|
+
else:
|
4456
|
+
convex_hull = Shell.ByFaces(faces, tolerance=tolerance, silent=True)
|
4457
|
+
if convex_hull:
|
4458
|
+
convex_hull_2 = Topology.RemoveCoplanarFaces(convex_hull, tolerance=tolerance, silent=True)
|
4459
|
+
if not convex_hull_2:
|
4460
|
+
return convex_hull
|
4461
|
+
else:
|
4462
|
+
convex_hull = Cluster.ByTopologies(faces)
|
4463
|
+
return convex_hull
|
4470
4464
|
|
4471
4465
|
@staticmethod
|
4472
4466
|
def Copy(topology, deep=False):
|
@@ -4499,6 +4493,63 @@ class Topology():
|
|
4499
4493
|
return_topology = Topology.SetDictionary(return_topology, d)
|
4500
4494
|
return return_topology
|
4501
4495
|
|
4496
|
+
@staticmethod
|
4497
|
+
def Diameter(topology, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
|
4498
|
+
"""
|
4499
|
+
Computes the diameter of the convex hull of the given topology.
|
4500
|
+
The diameter is the maximum distance between any two vertices on the convex hull.
|
4501
|
+
|
4502
|
+
Parameters
|
4503
|
+
----------
|
4504
|
+
topology : topologic_core.Topology
|
4505
|
+
The input topology
|
4506
|
+
mantissa : int , optional
|
4507
|
+
The desired length of the mantissa. The default is 6.
|
4508
|
+
tolerance : float , optional
|
4509
|
+
The desired tolerance. The default is 0.0001.
|
4510
|
+
silent : bool , optional
|
4511
|
+
If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
|
4512
|
+
|
4513
|
+
Returns
|
4514
|
+
-------
|
4515
|
+
float
|
4516
|
+
The diameter of the convex hull of the topology.
|
4517
|
+
"""
|
4518
|
+
from topologicpy.Vertex import Vertex
|
4519
|
+
from topologicpy.Edge import Edge
|
4520
|
+
|
4521
|
+
if not Topology.IsInstance(topology, "Topology"):
|
4522
|
+
if not silent:
|
4523
|
+
print("Topology.Diameter - Error: The input topology parameter is not a valid Topology. Returning None.")
|
4524
|
+
return None
|
4525
|
+
if Topology.IsInstance(topology, "Vertex"):
|
4526
|
+
return 0.0
|
4527
|
+
if Topology.IsInstance(topology, "Edge"):
|
4528
|
+
sv = Edge.StartVertex(topology)
|
4529
|
+
ev = Edge.EndVertex(topology)
|
4530
|
+
return Vertex.Distance(sv, ev, mantissa=mantissa)
|
4531
|
+
|
4532
|
+
# Step 1: Get the convex hull
|
4533
|
+
hull = Topology.ConvexHull(topology, tolerance=tolerance, mantissa=mantissa, silent=silent)
|
4534
|
+
if not hull:
|
4535
|
+
return 0.0
|
4536
|
+
|
4537
|
+
# Step 2: Get all unique vertices of the hull
|
4538
|
+
vertices = Topology.Vertices(hull)
|
4539
|
+
num_vertices = len(vertices)
|
4540
|
+
if num_vertices < 2:
|
4541
|
+
return 0.0
|
4542
|
+
|
4543
|
+
# Step 3: Find the furthest pair
|
4544
|
+
max_distance = 0.0
|
4545
|
+
for i in range(num_vertices):
|
4546
|
+
for j in range(i + 1, num_vertices):
|
4547
|
+
dist = Vertex.Distance(vertices[i], vertices[j], mantissa=mantissa)
|
4548
|
+
if dist > max_distance:
|
4549
|
+
max_distance = dist
|
4550
|
+
|
4551
|
+
return round(max_distance, mantissa)
|
4552
|
+
|
4502
4553
|
@staticmethod
|
4503
4554
|
def Dictionary(topology):
|
4504
4555
|
"""
|
@@ -7429,7 +7480,7 @@ class Topology():
|
|
7429
7480
|
distances = []
|
7430
7481
|
for v in vertices:
|
7431
7482
|
distances.append(Vertex.PerpendicularDistance(v, face=face2, mantissa=6))
|
7432
|
-
d = sum(distances)/len(distances)
|
7483
|
+
d = sum(distances) / len(distances) if distances else float('inf')
|
7433
7484
|
return d <= epsilon
|
7434
7485
|
|
7435
7486
|
def cluster_faces_on_planes(faces, epsilon=1e-6):
|
@@ -8928,7 +8979,8 @@ class Topology():
|
|
8928
8979
|
intensities=intensities,
|
8929
8980
|
colorScale=colorScale,
|
8930
8981
|
mantissa=mantissa,
|
8931
|
-
tolerance=tolerance
|
8982
|
+
tolerance=tolerance,
|
8983
|
+
silent=silent))
|
8932
8984
|
topology_counter += offset
|
8933
8985
|
figure = Plotly.FigureByData(data=data, width=width, height=height,
|
8934
8986
|
xAxis=xAxis, yAxis=yAxis, zAxis=zAxis, axisSize=axisSize,
|
@@ -10149,7 +10201,7 @@ class Topology():
|
|
10149
10201
|
selectors = []
|
10150
10202
|
for aFace in topologyFaces:
|
10151
10203
|
if len(Topology.Vertices(aFace)) > 3:
|
10152
|
-
triFaces = Face.Triangulate(aFace, mode=mode, meshSize=meshSize, tolerance=tolerance)
|
10204
|
+
triFaces = Face.Triangulate(aFace, mode=mode, meshSize=meshSize, tolerance=tolerance, silent=silent)
|
10153
10205
|
else:
|
10154
10206
|
triFaces = [aFace]
|
10155
10207
|
for triFace in triFaces:
|
topologicpy/Vertex.py
CHANGED
@@ -152,6 +152,50 @@ class Vertex():
|
|
152
152
|
return False
|
153
153
|
return True
|
154
154
|
|
155
|
+
@staticmethod
|
156
|
+
def AreCoplanar(vertices: list, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False) -> bool:
|
157
|
+
"""
|
158
|
+
Returns True if the input list of vertices are coplanar. Returns False otherwise.
|
159
|
+
|
160
|
+
Parameters
|
161
|
+
----------
|
162
|
+
vertices : list
|
163
|
+
The input list of vertices.
|
164
|
+
mantissa : int , optional
|
165
|
+
The desired length of the mantissa. The default is 6.
|
166
|
+
tolerance : float , optional
|
167
|
+
The desired tolerance. The default is 0.0001
|
168
|
+
silent : bool , optional
|
169
|
+
If set to True, no warnings or errors are printed. The default is False.
|
170
|
+
|
171
|
+
Returns
|
172
|
+
-------
|
173
|
+
bool
|
174
|
+
True if the input vertices are coplanar.
|
175
|
+
|
176
|
+
"""
|
177
|
+
from topologicpy.Topology import Topology
|
178
|
+
from numpy.linalg import svd
|
179
|
+
|
180
|
+
if not isinstance(vertices, list):
|
181
|
+
if not silent:
|
182
|
+
print("Vertex.Normal - Error: The vertices input parameter is not a valid list. Returning None.")
|
183
|
+
return None
|
184
|
+
|
185
|
+
verts = [v for v in vertices if Topology.IsInstance(v, "vertex")]
|
186
|
+
|
187
|
+
if len(verts) < 3:
|
188
|
+
if not silent:
|
189
|
+
print("Vertex.AreCoplanar - Error: The list of vertices contains less than 3 valid topologic vertices. Returning None.")
|
190
|
+
return None # At least 3 vertices are needed
|
191
|
+
|
192
|
+
coords = np.array([Vertex.Coordinates(v, mantissa=mantissa) for v in vertices])
|
193
|
+
|
194
|
+
# Check if points are coplanar using SVD
|
195
|
+
_, s, vh = svd(coords - coords.mean(axis=0))
|
196
|
+
rank = (s > tolerance).sum()
|
197
|
+
return (rank <= 2)
|
198
|
+
|
155
199
|
@staticmethod
|
156
200
|
def AreIpsilateral(vertices: list, face) -> bool:
|
157
201
|
"""
|
@@ -1453,6 +1497,57 @@ class Vertex():
|
|
1453
1497
|
sorted_indices = [x for _, x in sorted(zip(distances, indices))]
|
1454
1498
|
return vertices[sorted_indices[0]]
|
1455
1499
|
|
1500
|
+
@staticmethod
|
1501
|
+
def Normal(vertices, mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
|
1502
|
+
"""
|
1503
|
+
Computes the normal vector of a list of co-planar Topologic vertices.
|
1504
|
+
Depending on the order of the vertices, the normal can be flipped 180 degrees.
|
1505
|
+
|
1506
|
+
Parameters
|
1507
|
+
----------
|
1508
|
+
vertices : list
|
1509
|
+
A list of Topologic Vertex objects that are assumed to be co-planar.
|
1510
|
+
mantissa : int, optional
|
1511
|
+
The desired length of the mantissa. The default is 6.
|
1512
|
+
tolerance : float, optional
|
1513
|
+
The desired tolerance. The default is 0.0001.
|
1514
|
+
silent : bool , optional
|
1515
|
+
If set to True no warnings or errors are printed. The default is False.
|
1516
|
+
|
1517
|
+
Returns
|
1518
|
+
-------
|
1519
|
+
list
|
1520
|
+
A unit normal vector [x, y, z] of the plane defined by the vertices, or None if invalid.
|
1521
|
+
"""
|
1522
|
+
from topologicpy.Vertex import Vertex
|
1523
|
+
from topologicpy.Topology import Topology
|
1524
|
+
import numpy as np
|
1525
|
+
|
1526
|
+
if not isinstance(vertices, list):
|
1527
|
+
if not silent:
|
1528
|
+
print("Vertex.Normal - Error: The vertices input parameter is not a valid list. Returning None.")
|
1529
|
+
return None
|
1530
|
+
|
1531
|
+
verts = [v for v in vertices if Topology.IsInstance(v, "vertex")]
|
1532
|
+
|
1533
|
+
if len(verts) < 3:
|
1534
|
+
if not silent:
|
1535
|
+
print("Vertex.Normal - Error: The list of vertices contains less than 3 valid topologic vertices. Returning None.")
|
1536
|
+
return None # At least 3 vertices are needed
|
1537
|
+
|
1538
|
+
coords = np.array([Vertex.Coordinates(v, mantissa=mantissa) for v in verts])
|
1539
|
+
centroid = np.mean(coords, axis=0)
|
1540
|
+
centered = coords - centroid
|
1541
|
+
|
1542
|
+
# Use SVD to find the normal as the vector corresponding to the smallest singular value
|
1543
|
+
_, _, vh = np.linalg.svd(centered)
|
1544
|
+
normal = vh[-1] # The last row is the normal of the best-fit plane
|
1545
|
+
|
1546
|
+
norm = np.linalg.norm(normal)
|
1547
|
+
if norm < tolerance:
|
1548
|
+
return None # Degenerate normal
|
1549
|
+
return list(np.round(normal / norm, mantissa))
|
1550
|
+
|
1456
1551
|
@staticmethod
|
1457
1552
|
def Origin():
|
1458
1553
|
"""
|
topologicpy/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = '0.8.
|
1
|
+
__version__ = '0.8.28'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: topologicpy
|
3
|
-
Version: 0.8.
|
3
|
+
Version: 0.8.28
|
4
4
|
Summary: An AI-Powered Spatial Modelling and Analysis Software Library for Architecture, Engineering, and Construction.
|
5
5
|
Author-email: Wassim Jabi <wassim.jabi@gmail.com>
|
6
6
|
License: AGPL v3 License
|
@@ -11,27 +11,27 @@ topologicpy/DGL.py,sha256=HQXy9iDnrvWGDxaBfe5pRbweQ2zLBvAf6UdjfhKkQYI,139041
|
|
11
11
|
topologicpy/Dictionary.py,sha256=Lf24WHW8q_RCq0l8VpT3XJTn6UuStY66JI4Lb4W08jI,34126
|
12
12
|
topologicpy/Edge.py,sha256=pu4tZbRbK8qx2oqRbwHAeKuwU2X8JFGPSJjJMTJw8Q0,71418
|
13
13
|
topologicpy/EnergyModel.py,sha256=Pyb28gDDwhzlQIH0xqAygqS0P3SJxWyyV7OWS_AAfRs,53856
|
14
|
-
topologicpy/Face.py,sha256=
|
15
|
-
topologicpy/Graph.py,sha256=
|
16
|
-
topologicpy/Grid.py,sha256=
|
14
|
+
topologicpy/Face.py,sha256=E1ffqvCNDWKFnFiDrEu70KWPGMGjtD3SoIxu4Yokn_I,186106
|
15
|
+
topologicpy/Graph.py,sha256=t_2lHDUdQHsG-PYVjeRZtqfjKv6eD8jI8ZJl9uQW_GU,574360
|
16
|
+
topologicpy/Grid.py,sha256=EbI2NcYhQDpD5mItd7A1Lpr8Puuf87vZPWuoh7_gChQ,18483
|
17
17
|
topologicpy/Helper.py,sha256=JdvC30WMrla46mTj5TdwCV_bRv-6y8vK5Bkx0prluy4,29100
|
18
18
|
topologicpy/Honeybee.py,sha256=yctkwfdupKnp7bAOjP1Z4YaYpRrWoMEb4gz9Z5zaWwE,21751
|
19
19
|
topologicpy/Matrix.py,sha256=BHGDRkBn1pf5DkRoY8feAhDGHTF3bjFM4jluiEb_A0w,22779
|
20
20
|
topologicpy/Neo4j.py,sha256=vNMaqTWerwr-3luLjYEXNhf8T97aFee6x5sIKBHY73s,22392
|
21
|
-
topologicpy/Plotly.py,sha256=
|
21
|
+
topologicpy/Plotly.py,sha256=s0fC_eNsgJN8zkkY_EomZi6jnKCIdtrVYsgnS51MWkU,119271
|
22
22
|
topologicpy/Polyskel.py,sha256=oVfM4lqSMPTjnkHfsRU9VI8Blt6Vf0LVPkD9ebz7Wmw,27082
|
23
23
|
topologicpy/PyG.py,sha256=zvV6jtnol_aFiN6JRoMpYwBVfOU2aFs9gdWSdEo6mtU,109757
|
24
|
-
topologicpy/Shell.py,sha256=
|
24
|
+
topologicpy/Shell.py,sha256=h8S2nP1e0JtMxeOdAFZVhOYTJWTW8vlZRM5lxK0gu2o,89577
|
25
25
|
topologicpy/Speckle.py,sha256=-eiTqJugd7pHiHpD3pDUcDO6CGhVyPV14HFRzaqEoaw,18187
|
26
26
|
topologicpy/Sun.py,sha256=_VBBAUIDhvpkp72JBZlv7k9qx9jYubm3yM56UZ1Nc6c,36837
|
27
|
-
topologicpy/Topology.py,sha256=
|
27
|
+
topologicpy/Topology.py,sha256=3wTW_C3lquA7lc5sLMxxoBj53Ygc--6hLl5LGvnv3NI,478412
|
28
28
|
topologicpy/Vector.py,sha256=mx7fgABdioikPWM9HzXKzmqfx3u_XBcU_jlLD4qK2x8,42407
|
29
|
-
topologicpy/Vertex.py,sha256=
|
29
|
+
topologicpy/Vertex.py,sha256=UMDhERrLH6b4WOu4pl0UgYzcfp9-NvmASLtKXwetO_4,84687
|
30
30
|
topologicpy/Wire.py,sha256=eRs4PM7h4yU5v6umPh0oBJR4cN8BwsqlVroaFdnvK4w,228499
|
31
31
|
topologicpy/__init__.py,sha256=RMftibjgAnHB1vdL-muo71RwMS4972JCxHuRHOlU428,928
|
32
|
-
topologicpy/version.py,sha256=
|
33
|
-
topologicpy-0.8.
|
34
|
-
topologicpy-0.8.
|
35
|
-
topologicpy-0.8.
|
36
|
-
topologicpy-0.8.
|
37
|
-
topologicpy-0.8.
|
32
|
+
topologicpy/version.py,sha256=YRGDu9QPaNPQlZcx7MJLLUrv2aIEpppZ8LAIiyZvW00,23
|
33
|
+
topologicpy-0.8.28.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
|
34
|
+
topologicpy-0.8.28.dist-info/METADATA,sha256=W4Rje6xhqAy7LxQUXrv0Q5yY97BFPOKC0iWuZDqjjJM,10535
|
35
|
+
topologicpy-0.8.28.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
36
|
+
topologicpy-0.8.28.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
|
37
|
+
topologicpy-0.8.28.dist-info/RECORD,,
|
File without changes
|
File without changes
|