topologicpy 0.8.26__py3-none-any.whl → 0.8.29__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 +12 -7
- topologicpy/ShapeGrammar.py +492 -0
- topologicpy/Shell.py +36 -0
- topologicpy/Topology.py +301 -251
- topologicpy/Vertex.py +95 -0
- topologicpy/version.py +1 -1
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.29.dist-info}/METADATA +1 -1
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.29.dist-info}/RECORD +14 -13
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.29.dist-info}/WHEEL +1 -1
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.29.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.26.dist-info → topologicpy-0.8.29.dist-info}/top_level.txt +0 -0
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,
|
@@ -9897,7 +9949,6 @@ class Topology():
|
|
9897
9949
|
|
9898
9950
|
# Extract translation (last column of the matrix)
|
9899
9951
|
translation = [m[3] for m in matrix[:3]]
|
9900
|
-
print("translation", translation)
|
9901
9952
|
x_translate, y_translate, z_translate = translation
|
9902
9953
|
|
9903
9954
|
# Extract rotation (top-left 3x3 part of the matrix)
|
@@ -9905,7 +9956,6 @@ class Topology():
|
|
9905
9956
|
|
9906
9957
|
# Extract scaling (diagonal of the matrix)
|
9907
9958
|
scaling_factors = [matrix[m][m] for m in [0,1,2]] # scaling is stored in the diagonal of the rotation matrix
|
9908
|
-
print(scaling_factors)
|
9909
9959
|
x_scale, y_scale, z_scale = scaling_factors
|
9910
9960
|
|
9911
9961
|
# Step 1: Apply Scaling
|
@@ -10149,7 +10199,7 @@ class Topology():
|
|
10149
10199
|
selectors = []
|
10150
10200
|
for aFace in topologyFaces:
|
10151
10201
|
if len(Topology.Vertices(aFace)) > 3:
|
10152
|
-
triFaces = Face.Triangulate(aFace, mode=mode, meshSize=meshSize, tolerance=tolerance)
|
10202
|
+
triFaces = Face.Triangulate(aFace, mode=mode, meshSize=meshSize, tolerance=tolerance, silent=silent)
|
10153
10203
|
else:
|
10154
10204
|
triFaces = [aFace]
|
10155
10205
|
for triFace in triFaces:
|