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 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
- print("Face.Isovist - Error: The viewpoint is not inside the face. Returning None.")
2473
- return None
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
- print("Face.Triangulate - Error: The input face parameter is not a valid face. Returning None.")
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
- print("Face.Triangulate - Error: The input face parameter is not a valid face. Returning None.")
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
- gridVertex = gridVertex.Intersect(face, False)
290
- if Topology.IsInstance(gridVertex, "Vertex"):
291
- d = Dictionary.ByKeysValues(["u","v"],[u,v])
292
- if d:
293
- gridVertex.SetDictionary(d)
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
- gridVertex = gridVertex.Intersect(face, False)
351
- if Topology.IsInstance(gridVertex, "Vertex"):
352
- d = Dictionary.ByKeysValues(["u","v"],[u,v])
353
- if d:
354
- gridVertex.SetDictionary(d)
355
- gridVertices.append(gridVertex)
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", mantissa=6, tolerance=0.0001):
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, removeCoplanarFaces=False, epsilon=0.0001, tolerance=0.0001):
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 list of topologies by importing them from an IFC file.
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
- The desired epsilon (another form of tolerance) for finding if two faces are coplanar. The default is 0.0001.
2139
- tolerance : float , optional
2140
- The desired tolerance. The default is 0.0001.
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, ifcopenshell.geom
2149
- from topologicpy.Dictionary import Dictionary
2165
+ import ifcopenshell
2166
+ import ifcopenshell.geom
2150
2167
  from topologicpy.Vertex import Vertex
2151
- from topologicpy.Edge import Edge
2168
+ from topologicpy.Wire import Wire
2152
2169
  from topologicpy.Face import Face
2153
- import numpy as np
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
- def get_psets(entity):
2156
- # Initialize the PSET dictionary for this entity
2157
- psets = {}
2158
-
2159
- # Check if the entity has a GlobalId
2160
- if not hasattr(entity, 'GlobalId'):
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
- # Set default Material Name and ID
2192
- material_list = []
2193
- # Set default transparency based on entity type or material
2194
- default_transparency = 0.0
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
- products = file.by_type("IfcProduct")
2337
- entities = []
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 entities:
2344
- topologies.append(convert_to_topology(entity, settings, transferDictionaries=transferDictionaries))
2345
- return topologies
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
- Creates a convex hull
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 , optional
4410
- The desired length of the mantissa. The default is 6.
4411
- tolerance : float , optional
4412
- The desired tolerance. The default is 0.0001.
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 created convex hull of the input topology.
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.Cluster import Cluster
4427
-
4428
- def convexHull3D(item, tolerance, option):
4429
- if item:
4430
- vertices = Topology.Vertices(item)
4431
- pointList = []
4432
- for v in vertices:
4433
- pointList.append(Vertex.Coordinates(v, mantissa=mantissa))
4434
- points = np.array(pointList)
4435
- if option:
4436
- hull = ConvexHull(points, qhull_options=option)
4437
- else:
4438
- hull = ConvexHull(points)
4439
- faces = []
4440
- for simplex in hull.simplices:
4441
- edges = []
4442
- for i in range(len(simplex)-1):
4443
- sp = hull.points[simplex[i]]
4444
- ep = hull.points[simplex[i+1]]
4445
- sv = Vertex.ByCoordinates(sp[0], sp[1], sp[2])
4446
- ev = Vertex.ByCoordinates(ep[0], ep[1], ep[2])
4447
- edges.append(Edge.ByVertices([sv, ev], tolerance=tolerance))
4448
- sp = hull.points[simplex[-1]]
4449
- ep = hull.points[simplex[0]]
4450
- sv = Vertex.ByCoordinates(sp[0], sp[1], sp[2])
4451
- ev = Vertex.ByCoordinates(ep[0], ep[1], ep[2])
4452
- edges.append(Edge.ByVertices([sv, ev], tolerance=tolerance))
4453
- faces.append(Face.ByWire(Wire.ByEdges(edges, tolerance=tolerance), tolerance=tolerance))
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
- c = Cell.ByFaces(faces, tolerance=tolerance)
4456
- return c
4389
+ hull = SciPyConvexHull(coords)
4457
4390
  except:
4458
- returnTopology = Topology.SelfMerge(Cluster.ByTopologies(faces), tolerance=tolerance)
4459
- if Topology.Type(returnTopology) == Topology.TypeID("Shell"):
4460
- return Shell.ExternalBoundary(returnTopology, tolerance=tolerance)
4461
- if not Topology.IsInstance(topology, "Topology"):
4462
- print("Topology.ConvexHull - Error: the input topology parameter is not a valid topology. Returning None.")
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
- returnObject = None
4465
- try:
4466
- returnObject = convexHull3D(topology, tolerance, None)
4467
- except:
4468
- returnObject = convexHull3D(topology, tolerance, 'QJ')
4469
- return returnObject
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.26'
1
+ __version__ = '0.8.28'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topologicpy
3
- Version: 0.8.26
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=7K46gB_UIKjKEKyzyY0JqGarqjwjH0ggS-JQTpDtWC4,184847
15
- topologicpy/Graph.py,sha256=9gK-dEMtjo7KOXbCACpgwOYWhgTAHy42g6WiQ-rw20M,563389
16
- topologicpy/Grid.py,sha256=qRnFUvs079zMOZ6COWzBX6408niI7HyNz-BM0VRguXY,18245
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=eHTKT8vOHJDLwH4xypj52atGLR0dGic45d-BiPA55Q8,119219
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=3zUSf0VfkAF73ZyTYNWYW21x4-vyfi7JkYu_r5GTVIc,87983
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=BKiVJ-R5FfwXJ2FYp1V8pjaPoMxRm83MGe2RzBx0_tQ,479205
27
+ topologicpy/Topology.py,sha256=3wTW_C3lquA7lc5sLMxxoBj53Ygc--6hLl5LGvnv3NI,478412
28
28
  topologicpy/Vector.py,sha256=mx7fgABdioikPWM9HzXKzmqfx3u_XBcU_jlLD4qK2x8,42407
29
- topologicpy/Vertex.py,sha256=PIwfbA7_TxK_dSGlSeM5mson97TRr4dYrfZyOLgO150,80913
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=rN0alF_hLv16P_Fcr4gxu7bHFSkbtvltSnm1X_HNZ5w,23
33
- topologicpy-0.8.26.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
34
- topologicpy-0.8.26.dist-info/METADATA,sha256=D7oAeLuTLnKLuMBL8WSrlBMwSSjsK5QpsodjjHXkc7E,10535
35
- topologicpy-0.8.26.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
36
- topologicpy-0.8.26.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
37
- topologicpy-0.8.26.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.4.0)
2
+ Generator: setuptools (80.7.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5