topologicpy 0.8.25__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/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: