topologicpy 0.8.91__py3-none-any.whl → 0.8.93__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/Cell.py CHANGED
@@ -1753,7 +1753,7 @@ class Cell():
1753
1753
  if placement.lower() == "bottom":
1754
1754
  egg = Topology.Translate(egg, 0, 0, height/2)
1755
1755
  elif placement.lower() == "lowerleft":
1756
- bb = Cell.BoundingBox(egg)
1756
+ bb = Topology.BoundingBox(egg)
1757
1757
  d = Topology.Dictionary(bb)
1758
1758
  width = Dictionary.ValueAtKey(d, 'width')
1759
1759
  length = Dictionary.ValueAtKey(d, 'length')
topologicpy/Topology.py CHANGED
@@ -838,10 +838,15 @@ class Topology():
838
838
  """
839
839
 
840
840
  from topologicpy.Dictionary import Dictionary
841
+ import inspect
841
842
 
842
843
  if not Topology.IsInstance(topology, "Topology"):
843
844
  if not silent:
844
845
  print("Topology.Apertures - Error: the input topology parameter is not a valid topology. Returning None.")
846
+ print("Topology:", topology)
847
+ curframe = inspect.currentframe()
848
+ calframe = inspect.getouterframes(curframe, 2)
849
+ print('caller name:', calframe[1][3])
845
850
  return None
846
851
 
847
852
  apertures = []
@@ -2521,39 +2526,41 @@ class Topology():
2521
2526
  f = Face.ByWires(eb, ib)
2522
2527
  new_faces.append(f)
2523
2528
  topology = Topology.SelfMerge(Cluster.ByTopologies(new_faces))
2524
- if removeCoplanarFaces:
2525
- topology = Topology.RemoveCoplanarFaces(topology, epsilon=epsilon, tolerance=tolerance)
2526
- if transferDictionaries:
2527
- element_dict = {
2528
- "TOPOLOGIC_id": str(Topology.UUID(topology)),
2529
- "TOPOLOGIC_name": getattr(element, 'Name', "Untitled"),
2530
- "TOPOLOGIC_type": Topology.TypeAsString(topology),
2531
- "IFC_global_id": getattr(element, 'GlobalId', 0),
2532
- "IFC_name": getattr(element, 'Name', "Untitled"),
2533
- "IFC_type": element_type
2534
- }
2535
-
2536
- # Optionally add property sets
2537
- psets = {}
2538
- if hasattr(element, 'IsDefinedBy'):
2539
- for rel in element.IsDefinedBy:
2540
- if rel.is_a('IfcRelDefinesByProperties'):
2541
- pdef = rel.RelatingPropertyDefinition
2542
- if pdef and pdef.is_a('IfcPropertySet'):
2543
- key = f"IFC_{pdef.Name}"
2544
- props = {}
2545
- for prop in pdef.HasProperties:
2546
- if prop.is_a('IfcPropertySingleValue') and prop.NominalValue:
2547
- props[f"IFC_{prop.Name}"] = prop.NominalValue.wrappedValue
2548
- psets[key] = props
2549
-
2550
- final_dict = Dictionary.ByPythonDictionary(element_dict)
2551
- if psets:
2552
- pset_dict = Dictionary.ByPythonDictionary(psets)
2553
- final_dict = Dictionary.ByMergedDictionaries([final_dict, pset_dict])
2554
-
2555
- topology = Topology.SetDictionary(topology, final_dict)
2556
- topologies.append(topology)
2529
+ if Topology.IsInstance(topology, "topology"):
2530
+ if removeCoplanarFaces:
2531
+ topology = Topology.RemoveCoplanarFaces(topology, epsilon=epsilon, tolerance=tolerance)
2532
+ if Topology.IsInstance(topology, "topology"):
2533
+ if transferDictionaries:
2534
+ element_dict = {
2535
+ "TOPOLOGIC_id": str(Topology.UUID(topology)),
2536
+ "TOPOLOGIC_name": getattr(element, 'Name', "Untitled"),
2537
+ "TOPOLOGIC_type": Topology.TypeAsString(topology),
2538
+ "IFC_global_id": getattr(element, 'GlobalId', 0),
2539
+ "IFC_name": getattr(element, 'Name', "Untitled"),
2540
+ "IFC_type": element_type
2541
+ }
2542
+
2543
+ # Optionally add property sets
2544
+ psets = {}
2545
+ if hasattr(element, 'IsDefinedBy'):
2546
+ for rel in element.IsDefinedBy:
2547
+ if rel.is_a('IfcRelDefinesByProperties'):
2548
+ pdef = rel.RelatingPropertyDefinition
2549
+ if pdef and pdef.is_a('IfcPropertySet'):
2550
+ key = f"IFC_{pdef.Name}"
2551
+ props = {}
2552
+ for prop in pdef.HasProperties:
2553
+ if prop.is_a('IfcPropertySingleValue') and prop.NominalValue:
2554
+ props[f"IFC_{prop.Name}"] = prop.NominalValue.wrappedValue
2555
+ psets[key] = props
2556
+
2557
+ final_dict = Dictionary.ByPythonDictionary(element_dict)
2558
+ if psets:
2559
+ pset_dict = Dictionary.ByPythonDictionary(psets)
2560
+ final_dict = Dictionary.ByMergedDictionaries([final_dict, pset_dict])
2561
+
2562
+ topology = Topology.SetDictionary(topology, final_dict)
2563
+ topologies.append(topology)
2557
2564
  if not it.next():
2558
2565
  break
2559
2566
 
@@ -11904,7 +11911,7 @@ class Topology():
11904
11911
  return Topology._Boolean(topologyA, topologyB, operation="union", tranDict=tranDict, tolerance=tolerance, silent=silent)
11905
11912
 
11906
11913
  @staticmethod
11907
- def UUID(topology, namespace="topologicpy"):
11914
+ def UUID(topology, namespace="topologicpy", silent: bool = False):
11908
11915
  """
11909
11916
  Generate a UUID v5 based on the provided content and a fixed namespace.
11910
11917
 
@@ -11913,7 +11920,9 @@ class Topology():
11913
11920
  topology : topologic_core.Topology
11914
11921
  The input topology
11915
11922
  namespace : str , optional
11916
- The base namescape to use for generating the UUID
11923
+ The base namescape to use for generating the UUID.
11924
+ silent : bool , optional
11925
+ If set to True, error and warning messages are suppressed. Default is False.
11917
11926
 
11918
11927
  Returns
11919
11928
  -------
@@ -11924,6 +11933,16 @@ class Topology():
11924
11933
  import uuid
11925
11934
  from topologicpy.Dictionary import Dictionary
11926
11935
  from topologicpy.Graph import Graph
11936
+ import inspect
11937
+
11938
+ if not Topology.IsInstance(topology, "topology"):
11939
+ if not silent:
11940
+ print("Topology.UUID - Error: The input topology parameter is not a valid topology. Returning None.")
11941
+ print("Topology:", topology)
11942
+ curframe = inspect.currentframe()
11943
+ calframe = inspect.getouterframes(curframe, 2)
11944
+ print('caller name:', calframe[1][3])
11945
+ return None
11927
11946
 
11928
11947
  predefined_namespace_dns = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
11929
11948
  namespace_uuid = uuid.uuid5(predefined_namespace_dns, namespace)
@@ -11935,14 +11954,14 @@ class Topology():
11935
11954
  final_str = verts_str+edges_str+dict_str
11936
11955
  uuid_str = uuid.uuid5(namespace_uuid, final_str)
11937
11956
  else:
11938
- cellComplexes = Topology.CellComplexes(topology)
11939
- cells = Topology.Cells(topology)
11940
- shells = Topology.Shells(topology)
11941
- faces = Topology.Faces(topology)
11942
- wires = Topology.Wires(topology)
11943
- edges = Topology.Edges(topology)
11944
- vertices = Topology.Vertices(topology)
11945
- apertures = Topology.Apertures(topology, subTopologyType="all")
11957
+ cellComplexes = Topology.CellComplexes(topology, silent=True) or []
11958
+ cells = Topology.Cells(topology, silent=True) or []
11959
+ shells = Topology.Shells(topology, silent=True) or []
11960
+ faces = Topology.Faces(topology, silent=True) or []
11961
+ wires = Topology.Wires(topology, silent=True) or []
11962
+ edges = Topology.Edges(topology, silent=True) or []
11963
+ vertices = Topology.Vertices(topology, silent=True) or []
11964
+ apertures = Topology.Apertures(topology, subTopologyType="all") or []
11946
11965
  subTopologies = cellComplexes+cells+shells+faces+wires+edges+vertices+apertures
11947
11966
  dictionaries = [Dictionary.PythonDictionary(Topology.Dictionary(topology))]
11948
11967
  dictionaries += [Dictionary.PythonDictionary(Topology.Dictionary(s)) for s in subTopologies]
topologicpy/Wire.py CHANGED
@@ -946,6 +946,168 @@ class Wire():
946
946
  vertices = Topology.Vertices(cluster)
947
947
  return Wire.ByVertices(vertices, close=close, tolerance=tolerance, silent=silent)
948
948
 
949
+
950
+ @staticmethod
951
+ def Cage(origin=None,
952
+ width: float = 1.0, length: float = 1.0, height: float = 1.0,
953
+ uSides: int = 2, vSides: int = 2, wSides: int = 2,
954
+ direction: list = [0, 0, 1], placement: str = "center",
955
+ mantissa: int = 6, tolerance: float = 0.0001):
956
+ """
957
+ Creates a prismatic 3D cage as a Wire, with edges only on the outer
958
+ surfaces of the volume (no interior lines).
959
+
960
+ Parameters
961
+ ----------
962
+ origin : topologic_core.Vertex , optional
963
+ The placement origin of the cage:
964
+ - If placement == "center": the geometric center of the cage
965
+ is placed at this origin.
966
+ - If placement == "corner": the minimum corner of the cage
967
+ is placed at this origin.
968
+ If None, the cage is created around (0, 0, 0) accordingly.
969
+ width : float , optional
970
+ The size of the cage in the local X direction. Default is 1.0.
971
+ length : float , optional
972
+ The size of the cage in the local Y direction. Default is 1.0.
973
+ height : float , optional
974
+ The size of the cage in the local Z direction. Default is 1.0.
975
+ uSides : int , optional
976
+ The number of subdivisions in the local X direction. Must be >= 1.
977
+ Default is 2.
978
+ vSides : int , optional
979
+ The number of subdivisions in the local Y direction. Must be >= 1.
980
+ Default is 2.
981
+ wSides : int , optional
982
+ The number of subdivisions in the local Z direction. Must be >= 1.
983
+ Default is 2.
984
+ direction : list , optional
985
+ The vector representing the up direction of the lattice. Default is [0, 0, 1].
986
+ placement : str , optional
987
+ The description of the placement of the origin of the lattice. This can be "bottom", "center", or "lowerleft". It is case insensitive. Default is "center".
988
+ mantissa : int , optional
989
+ The number of decimal places to round the result to. Default is 6.
990
+ tolerance : float , optional
991
+ The desired tolerance. Default is 0.0001.
992
+
993
+ Returns
994
+ -------
995
+ topologic_core.Wire or None
996
+ The resulting cage Wire, or None if inputs are invalid.
997
+ """
998
+ from topologicpy.Vertex import Vertex
999
+ from topologicpy.Edge import Edge
1000
+ from topologicpy.Wire import Wire
1001
+ from topologicpy.Topology import Topology
1002
+ from topologicpy.Vector import Vector
1003
+ import math
1004
+
1005
+ # -------------------------
1006
+ # Validation
1007
+ # -------------------------
1008
+ if uSides < 1 or vSides < 1 or wSides < 1:
1009
+ print("Wire.Cage - Error: uSides, vSides, and wSides must be >= 1. Returning None.")
1010
+ return None
1011
+
1012
+ if origin is None:
1013
+ origin = Vertex.ByCoordinates(0, 0, 0)
1014
+
1015
+ # Local origin at (0,0,0) for construction and rotation
1016
+ local_origin = Vertex.ByCoordinates(0, 0, 0)
1017
+
1018
+ # -------------------------
1019
+ # Local Placement Offsets
1020
+ # -------------------------
1021
+ # We construct the cage in a local coordinate system.
1022
+ if str(placement).lower() == "center":
1023
+ ox = -width * 0.5
1024
+ oy = -length * 0.5
1025
+ oz = -height * 0.5
1026
+ elif str(placement).lower() == "bottom":
1027
+ ox = -width * 0.5
1028
+ oy = -length * 0.5
1029
+ oz = 0
1030
+ else: # "lowerleft"
1031
+ ox = 0.0
1032
+ oy = 0.0
1033
+ oz = 0.0
1034
+
1035
+ # -------------------------
1036
+ # Step Sizes
1037
+ # -------------------------
1038
+ du = width / uSides
1039
+ dv = length / vSides
1040
+ dw = height / wSides
1041
+
1042
+ # -------------------------
1043
+ # Grid Coordinates (local)
1044
+ # -------------------------
1045
+ xs = [round(ox + i * du, mantissa) for i in range(uSides + 1)]
1046
+ ys = [round(oy + j * dv, mantissa) for j in range(vSides + 1)]
1047
+ zs = [round(oz + k * dw, mantissa) for k in range(wSides + 1)]
1048
+
1049
+ edges = []
1050
+
1051
+ # ------------------------------------------------------------------
1052
+ # X-direction edges on boundary surfaces (y,z)
1053
+ # Edge from (x_min, y_j, z_k) to (x_max, y_j, z_k)
1054
+ # Only if j is boundary OR k is boundary → lies on outer surface.
1055
+ # ------------------------------------------------------------------
1056
+ for j in range(vSides + 1):
1057
+ for k in range(wSides + 1):
1058
+ if j in (0, vSides) or k in (0, wSides):
1059
+ y = ys[j]
1060
+ z = zs[k]
1061
+ v0 = Vertex.ByCoordinates(xs[0], y, z)
1062
+ v1 = Vertex.ByCoordinates(xs[-1], y, z)
1063
+ edges.append(Edge.ByVertices(v0, v1))
1064
+
1065
+ # ------------------------------------------------------------------
1066
+ # Y-direction edges on boundary surfaces (x,z)
1067
+ # Edge from (x_i, y_min, z_k) to (x_i, y_max, z_k)
1068
+ # Only if i is boundary OR k is boundary.
1069
+ # ------------------------------------------------------------------
1070
+ for i in range(uSides + 1):
1071
+ for k in range(wSides + 1):
1072
+ if i in (0, uSides) or k in (0, wSides):
1073
+ x = xs[i]
1074
+ z = zs[k]
1075
+ v0 = Vertex.ByCoordinates(x, ys[0], z)
1076
+ v1 = Vertex.ByCoordinates(x, ys[-1], z)
1077
+ edges.append(Edge.ByVertices(v0, v1))
1078
+
1079
+ # ------------------------------------------------------------------
1080
+ # Z-direction edges on boundary surfaces (x,y)
1081
+ # Edge from (x_i, y_j, z_min) to (x_i, y_j, z_max)
1082
+ # Only if i is boundary OR j is boundary.
1083
+ # ------------------------------------------------------------------
1084
+ for i in range(uSides + 1):
1085
+ for j in range(vSides + 1):
1086
+ if i in (0, uSides) or j in (0, vSides):
1087
+ x = xs[i]
1088
+ y = ys[j]
1089
+ v0 = Vertex.ByCoordinates(x, y, zs[0])
1090
+ v1 = Vertex.ByCoordinates(x, y, zs[-1])
1091
+ edges.append(Edge.ByVertices(v0, v1))
1092
+
1093
+ # -------------------------
1094
+ # Build Wire in Local Space
1095
+ # -------------------------
1096
+ if not edges:
1097
+ print("Wire.Cage - Warning: No edges created. Returning None.")
1098
+ return None
1099
+
1100
+ cage = Wire.ByEdges(edges)
1101
+
1102
+ # -------------------------
1103
+ # Orient and Place
1104
+ # -------------------------
1105
+ cage = Topology.Orient(cage, origin=Vertex.Origin(), dirA=[0, 0, 1], dirB=direction)
1106
+ cage = Topology.Place(cage, originA=Vertex.Origin(), originB=origin)
1107
+
1108
+ return cage
1109
+
1110
+
949
1111
  @staticmethod
950
1112
  def Circle(origin= None, radius: float = 0.5, sides: int = 16, fromAngle: float = 0.0, toAngle: float = 360.0, close: bool = True, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001, silent: bool = False):
951
1113
  """
@@ -3199,6 +3361,124 @@ class Wire():
3199
3361
  i_shape = Topology.Orient(i_shape, origin=origin, dirA=[0, 0, 1], dirB=direction)
3200
3362
  return i_shape
3201
3363
 
3364
+
3365
+
3366
+ @staticmethod
3367
+ def Lattice(origin=None,
3368
+ width: float = 1.0, length: float = 1.0, height: float = 1.0,
3369
+ uSides: int = 2, vSides: int = 2, wSides: int = 2,
3370
+ direction: list = [0, 0, 1], placement: str = "center",
3371
+ mantissa: int = 6, tolerance: float = 0.0001):
3372
+ """
3373
+ Creates a prismatic 3D lattice as a Wire.
3374
+
3375
+ Parameters
3376
+ ----------
3377
+ origin : topologic_core.Vertex , optional
3378
+ Placement origin.
3379
+ width, length, height : float
3380
+ Lattice extents.
3381
+ uSides, vSides, wSides : int
3382
+ Divisions along X, Y, Z.
3383
+ direction : list , optional
3384
+ The vector representing the up direction of the lattice. Default is [0, 0, 1].
3385
+ placement : str , optional
3386
+ The description of the placement of the origin of the lattice. This can be "bottom", "center", or "lowerleft". It is case insensitive. Default is "center".
3387
+ mantissa : int , optional
3388
+ The number of decimal places to round the result to. Default is 6.
3389
+ tolerance : float , optional
3390
+ The desired tolerance. Default is 0.0001.
3391
+
3392
+ Returns
3393
+ -------
3394
+ topologic_core.Wire
3395
+ """
3396
+
3397
+ from topologicpy.Vertex import Vertex
3398
+ from topologicpy.Edge import Edge
3399
+ from topologicpy.Wire import Wire
3400
+ from topologicpy.Topology import Topology
3401
+ from topologicpy.Vector import Vector
3402
+ import math
3403
+
3404
+ # -------------------------
3405
+ # Validation
3406
+ # -------------------------
3407
+ if uSides < 1 or vSides < 1 or wSides < 1:
3408
+ return None
3409
+
3410
+ if origin is None:
3411
+ origin = Vertex.ByCoordinates(0, 0, 0)
3412
+
3413
+ # -------------------------
3414
+ # Placement Offsets
3415
+ # -------------------------
3416
+ if placement.lower() == "center":
3417
+ ox = -width * 0.5
3418
+ oy = -length * 0.5
3419
+ oz = -height * 0.5
3420
+ elif placement.lower() == "bottom":
3421
+ ox = -width * 0.5
3422
+ oy = -length * 0.5
3423
+ oz = 0
3424
+ else:
3425
+ ox = oy = oz = 0.0
3426
+
3427
+ # -------------------------
3428
+ # Step Sizes
3429
+ # -------------------------
3430
+ du = width / uSides
3431
+ dv = length / vSides
3432
+ dw = height / wSides
3433
+
3434
+ # -------------------------
3435
+ # Precompute Grid Coordinates
3436
+ # -------------------------
3437
+ xs = [round(ox + i * du, mantissa) for i in range(uSides + 1)]
3438
+ ys = [round(oy + j * dv, mantissa) for j in range(vSides + 1)]
3439
+ zs = [round(oz + k * dw, mantissa) for k in range(wSides + 1)]
3440
+
3441
+ edges = []
3442
+
3443
+ # -------------------------
3444
+ # X-Direction Lines
3445
+ # -------------------------
3446
+ for y in ys:
3447
+ for z in zs:
3448
+ v0 = Vertex.ByCoordinates(xs[0], y, z)
3449
+ v1 = Vertex.ByCoordinates(xs[-1], y, z)
3450
+ edges.append(Edge.ByVertices(v0, v1))
3451
+
3452
+ # -------------------------
3453
+ # Y-Direction Lines
3454
+ # -------------------------
3455
+ for x in xs:
3456
+ for z in zs:
3457
+ v0 = Vertex.ByCoordinates(x, ys[0], z)
3458
+ v1 = Vertex.ByCoordinates(x, ys[-1], z)
3459
+ edges.append(Edge.ByVertices(v0, v1))
3460
+
3461
+ # -------------------------
3462
+ # Z-Direction Lines
3463
+ # -------------------------
3464
+ for x in xs:
3465
+ for y in ys:
3466
+ v0 = Vertex.ByCoordinates(x, y, zs[0])
3467
+ v1 = Vertex.ByCoordinates(x, y, zs[-1])
3468
+ edges.append(Edge.ByVertices(v0, v1))
3469
+
3470
+ # -------------------------
3471
+ # Build Wire
3472
+ # -------------------------
3473
+ lattice = Wire.ByEdges(edges)
3474
+
3475
+ # -------------------------
3476
+ # Orient and Place
3477
+ # -------------------------
3478
+ lattice = Topology.Orient(lattice, origin=Vertex.Origin(), dirA=[0, 0, 1], dirB=direction)
3479
+ lattice = Topology.Place(lattice, originA=Vertex.Origin(), originB=origin)
3480
+ return lattice
3481
+
3202
3482
  @staticmethod
3203
3483
  def Length(wire, mantissa: int = 6) -> float:
3204
3484
  """
@@ -4964,34 +5244,32 @@ class Wire():
4964
5244
  return sv
4965
5245
 
4966
5246
  @staticmethod
4967
- def StraightenInFace(wire, face, tolerance: float = 0.0001):
5247
+ def Straighten(wire, host, obstacles: list = None, portals: list = None,
5248
+ tolerance: float = 0.0001, silent: bool = False):
4968
5249
  """
4969
5250
  Returns a new Wire obtained by recursively replacing segments of the
4970
- input wire with the longest possible straight edge that is fully
4971
- embedded in the given face.
4972
-
4973
- For each starting vertex v_i along the wire, this method searches for
4974
- the furthest vertex v_j (j > i) such that the straight Edge between
4975
- v_i and v_j satisfies:
4976
-
4977
- Topology.Difference(edge, face) == None
4978
-
4979
- i.e. the edge lies completely within (or on the boundary of) the face.
4980
- All edges of the original wire between vertex indices i and j are then
4981
- replaced by this straight edge, and the process is repeated recursively
4982
- from index j.
5251
+ input wire with the longest possible straight edge that:
5252
+ 1. Is fully embedded in the given host.
5253
+ 2. Avoids intersection with an optional list of obstacle topologies.
5254
+ 3. Continues to pass through (intersects) an optional list of portal
5255
+ topologies that the original input wire intersects.
4983
5256
 
4984
5257
  Parameters
4985
5258
  ----------
4986
5259
  wire : topologic_core.Wire
4987
5260
  The input path wire whose vertices define the route to be
4988
5261
  straightened.
4989
- face : topologic_core.Face
4990
- The face within which the straightened edges must lie.
5262
+ host : topologic_core.Topology
5263
+ The host within which the straightened edges must lie.
5264
+ obstacles : list, optional
5265
+ The list of topologies with which the straightened edges must not intersect.
5266
+ portals : list, optional
5267
+ The list of topologies with which the straightened edges must intersect.
5268
+ Portals with which the original wire does NOT intersect are ignored.
4991
5269
  tolerance : float , optional
4992
- Numerical tolerance used for internal robustness checks. The
4993
- Topology.Difference call itself is left with its default tolerance.
4994
- Default is 1e-6.
5270
+ The desired tolerance. Default is 0.0001.
5271
+ silent : bool , optional
5272
+ If set to True, error and warning messages are suppressed. Default is False.
4995
5273
 
4996
5274
  Returns
4997
5275
  -------
@@ -5001,8 +5279,45 @@ class Wire():
5001
5279
  from topologicpy.Vertex import Vertex
5002
5280
  from topologicpy.Edge import Edge
5003
5281
  from topologicpy.Wire import Wire
5004
- from topologicpy.Face import Face
5282
+ from topologicpy.Cluster import Cluster
5005
5283
  from topologicpy.Topology import Topology
5284
+
5285
+ # Defensive defaults
5286
+ if obstacles is None:
5287
+ obstacles = []
5288
+ if portals is None:
5289
+ portals = []
5290
+
5291
+ # ----------------------------------------------------------------------
5292
+ # Basic validation
5293
+ # ----------------------------------------------------------------------
5294
+ if not Topology.IsInstance(wire, "Wire"):
5295
+ if not silent:
5296
+ print("Wire.Straighten - Error: The input wire parameter is not a valid Wire. Returning None.")
5297
+ return None
5298
+
5299
+ if not Topology.IsInstance(host, "Topology"):
5300
+ if not silent:
5301
+ print("Wire.Straighten - Error: The input host parameter is not a valid Topology. Returning None.")
5302
+ return None
5303
+
5304
+ if not isinstance(portals, list):
5305
+ if not silent:
5306
+ print("Wire.Straighten - Error: The input portals parameter is not a valid list. Returning None.")
5307
+ return None
5308
+
5309
+ if not isinstance(obstacles, list):
5310
+ if not silent:
5311
+ print("Wire.Straighten - Error: The input obstacles parameter is not a valid list. Returning None.")
5312
+ return None
5313
+
5314
+ # Filter valid obstacles and portals
5315
+ obstacle_list = [o for o in obstacles if Topology.IsInstance(o, "Topology")]
5316
+ portal_list = [p for p in portals if Topology.IsInstance(p, "Topology")]
5317
+
5318
+ # Make a cluster of the obstacles (if any)
5319
+ ob_cluster = Cluster.ByTopologies(obstacle_list) if obstacle_list else None
5320
+
5006
5321
  # Get ordered vertices of the wire
5007
5322
  vertices = Topology.Vertices(wire)
5008
5323
  n = len(vertices)
@@ -5011,75 +5326,249 @@ class Wire():
5011
5326
  # Nothing to straighten
5012
5327
  return wire
5013
5328
 
5014
- def _edge_inside_face(v_start, v_end):
5329
+ # ----------------------------------------------------------------------
5330
+ # Helper: check if a straight edge between two vertices is valid
5331
+ # ----------------------------------------------------------------------
5332
+ def _edge_is_valid(v_start, v_end):
5015
5333
  """
5016
5334
  Returns True if the straight edge between v_start and v_end is
5017
- fully embedded in the face, i.e. Topology.Difference(edge, face)
5018
- returns None.
5335
+ fully embedded in the host and does not intersect the obstacles.
5019
5336
  """
5020
- if v_start is v_end:
5337
+ # Avoid constructing degenerate edges
5338
+ if Topology.IsSame(v_start, v_end):
5021
5339
  return True
5022
- edge = Edge.ByStartVertexEndVertex(v_start, v_end)
5023
- diff = Topology.Difference(edge, face)
5024
- return diff is None
5025
5340
 
5026
- def _find_longest_valid_index(start_idx):
5027
- """
5028
- For a fixed start_idx, search for the largest index j >= start_idx+1
5029
- such that the direct edge (vertices[start_idx], vertices[j]) is
5030
- fully inside the face.
5341
+ edge = Edge.ByStartVertexEndVertex(v_start, v_end, tolerance=tolerance)
5342
+ if not Topology.IsInstance(edge, "Edge"):
5343
+ return False
5344
+
5345
+ diff = Topology.Difference(edge, host)
5346
+ if diff is not None:
5347
+ # Part of the edge lies outside the host
5348
+ return False
5349
+
5350
+ if ob_cluster is not None:
5351
+ inter = Topology.Intersect(edge, ob_cluster)
5352
+ if inter is not None:
5353
+ # Edge hits an obstacle
5354
+ return False
5031
5355
 
5032
- If for any reason no such j exists (which should not happen if the
5033
- original wire lies in the face), it falls back to start_idx + 1.
5356
+ return True
5357
+
5358
+ # ----------------------------------------------------------------------
5359
+ # Helper: for a fixed start index, find the furthest valid vertex index
5360
+ # ----------------------------------------------------------------------
5361
+ def _find_longest_valid_index(start_idx, local_vertices):
5362
+ """
5363
+ Given a list of vertices local_vertices (a sub-path),
5364
+ for a fixed start_idx, search for the largest index j >= start_idx+1
5365
+ such that the direct edge (local_vertices[start_idx], local_vertices[j])
5366
+ is valid.
5034
5367
  """
5035
- v_start = vertices[start_idx]
5368
+ m = len(local_vertices)
5369
+ v_start = local_vertices[start_idx]
5036
5370
  best_j = None
5037
5371
 
5038
- for j in range(start_idx + 1, n):
5039
- v_end = vertices[j]
5040
- if _edge_inside_face(v_start, v_end):
5372
+ for j in range(start_idx + 1, m):
5373
+ v_end = local_vertices[j]
5374
+ if _edge_is_valid(v_start, v_end):
5041
5375
  best_j = j
5042
- # Do NOT break on failure: a further vertex might still
5043
- # be reachable by a straight edge that stays in the face.
5376
+ # Do NOT break on failure: a further vertex might still be valid.
5044
5377
 
5045
5378
  if best_j is None:
5046
5379
  # Fallback: use the immediate next vertex to avoid stalling
5047
- best_j = min(start_idx + 1, n - 1)
5380
+ best_j = min(start_idx + 1, m - 1)
5048
5381
 
5049
5382
  return best_j
5050
5383
 
5051
- def _straighten_recursive(start_idx, out_vertices):
5384
+ # ----------------------------------------------------------------------
5385
+ # Helper: straighten a list of vertices (single segment, no portals)
5386
+ # ----------------------------------------------------------------------
5387
+ def _straighten_vertices(local_vertices):
5052
5388
  """
5053
- Recursive helper.
5389
+ Straightens a simple path defined by local_vertices (no portal constraints).
5390
+ Returns a new list of vertices.
5391
+ """
5392
+ m = len(local_vertices)
5393
+ if m <= 2:
5394
+ return local_vertices[:]
5395
+
5396
+ out_vertices = []
5397
+ idx = 0
5398
+ while idx < m - 1:
5399
+ out_vertices.append(local_vertices[idx])
5400
+ idx = _find_longest_valid_index(idx, local_vertices)
5401
+
5402
+ # Ensure the last vertex is present
5403
+ if not Topology.IsSame(out_vertices[-1], local_vertices[-1]):
5404
+ out_vertices.append(local_vertices[-1])
5405
+
5406
+ return out_vertices
5407
+
5408
+ # ----------------------------------------------------------------------
5409
+ # Portal support
5410
+ # ----------------------------------------------------------------------
5411
+ def _portal_cuts():
5412
+ """
5413
+ Returns a sorted list of (u, v_on_wire) where:
5414
+ - u is the parameter along the wire in [0, 1]
5415
+ - v_on_wire is a vertex on the wire at the same location
5054
5416
 
5055
- Appends the chosen vertices to out_vertices. At each step, it
5056
- decides how far it can jump from start_idx with a single straight
5057
- edge inside the face, then recurses from that new index.
5417
+ Only portals that actually intersect the original wire are considered.
5058
5418
  """
5059
- # Base case: we are at the last vertex
5060
- if start_idx == n - 1:
5061
- out_vertices.append(vertices[start_idx])
5062
- return
5419
+ cuts = []
5063
5420
 
5064
- # Find furthest valid index reachable from start_idx
5065
- next_idx = _find_longest_valid_index(start_idx)
5421
+ if not portal_list:
5422
+ return cuts
5066
5423
 
5067
- # Add the starting vertex for this segment
5068
- out_vertices.append(vertices[start_idx])
5424
+ for portal in portal_list:
5425
+ inter = Topology.Intersect(wire, portal)
5426
+ if not Topology.IsInstance(inter, "Topology"):
5427
+ # This portal does not intersect the wire, ignore it
5428
+ continue
5069
5429
 
5070
- # Recurse from the chosen furthest index
5071
- _straighten_recursive(next_idx, out_vertices)
5430
+ centroid = Topology.Centroid(inter)
5431
+ if not Topology.IsInstance(centroid, "Vertex"):
5432
+ continue
5072
5433
 
5073
- # Run the recursion
5074
- new_vertices = []
5075
- _straighten_recursive(0, new_vertices)
5434
+ # First try parameter at centroid directly
5435
+ u_target = Wire.ParameterAtVertex(wire, centroid, silent=True)
5436
+
5437
+ if u_target is not None:
5438
+ v_on_wire = centroid
5439
+ else:
5440
+ # Fall back to the closest point on the wire
5441
+ shortest_edge = Topology.ShortestEdge(centroid, wire, silent=True)
5442
+ if not Topology.IsInstance(shortest_edge, "Edge"):
5443
+ # Can't locate a good cut point for this portal
5444
+ continue
5445
+ v_on_wire = Edge.EndVertex(shortest_edge)
5446
+ if not Topology.IsInstance(v_on_wire, "Vertex"):
5447
+ continue
5448
+ u_target = Wire.ParameterAtVertex(wire, v_on_wire, silent=True)
5449
+
5450
+ # If still None, skip this portal
5451
+ if u_target is None:
5452
+ continue
5453
+
5454
+ # Keep u in [0,1], ignoring exact endpoints
5455
+ if 0.0 < u_target < 1.0:
5456
+ cuts.append((u_target, v_on_wire))
5457
+
5458
+ # Sort by parameter, ensure uniqueness
5459
+ cuts = sorted(cuts, key=lambda x: x[0])
5460
+ unique_cuts = []
5461
+ last_u = None
5462
+ for u, v in cuts:
5463
+ if last_u is None or abs(u - last_u) > tolerance:
5464
+ unique_cuts.append((u, v))
5465
+ last_u = u
5466
+
5467
+ return unique_cuts
5468
+
5469
+ def _subdivide_by_portals():
5470
+ """
5471
+ Splits the original wire into sub-wires between portal cuts.
5472
+ Each sub-wire is a wire segment between:
5473
+ start -> first portal,
5474
+ portal i -> portal i+1,
5475
+ last portal -> end.
5476
+ """
5477
+ cuts = _portal_cuts()
5478
+ if not cuts:
5479
+ return [wire] # No usable portal intersections
5480
+
5481
+ # Extract only parameters, append 0.0 and 1.0 for full coverage
5482
+ params = [u for (u, _) in cuts]
5483
+ params = [0.0] + params + [1.0]
5076
5484
 
5077
- # In case of any numerical quirks, ensure the last original vertex is present
5078
- if new_vertices[-1] is not vertices[-1]:
5079
- new_vertices.append(vertices[-1])
5485
+ verts_orig = Topology.Vertices(wire)
5486
+ sub_wires = []
5080
5487
 
5081
- # Build the new straightened wire
5082
- return Wire.ByVertices(new_vertices, close=False)
5488
+ for a, b in zip(params[:-1], params[1:]):
5489
+ if b - a <= tolerance:
5490
+ continue # Degenerate segment
5491
+
5492
+ # Build the vertex list for this segment
5493
+ seg_vertices = []
5494
+
5495
+ # Start vertex at parameter a
5496
+ v_a = Wire.VertexByParameter(wire, a)
5497
+ if Topology.IsInstance(v_a, "Vertex"):
5498
+ seg_vertices.append(v_a)
5499
+
5500
+ # Intermediate original vertices whose parameter lies between a and b
5501
+ for v in verts_orig:
5502
+ u = Wire.ParameterAtVertex(wire, v, silent=True)
5503
+ if u is None:
5504
+ continue
5505
+ if a < u < b:
5506
+ seg_vertices.append(v)
5507
+
5508
+ # End vertex at parameter b
5509
+ v_b = Wire.VertexByParameter(wire, b)
5510
+ if Topology.IsInstance(v_b, "Vertex"):
5511
+ seg_vertices.append(v_b)
5512
+
5513
+ # Make sure we have at least two vertices
5514
+ if len(seg_vertices) >= 2:
5515
+ sub_wires.append(Wire.ByVertices(seg_vertices, close=False, silent=True))
5516
+
5517
+ if not sub_wires:
5518
+ # Fallback: return the original wire if subdivision failed
5519
+ return [wire]
5520
+
5521
+ return sub_wires
5522
+
5523
+ # ----------------------------------------------------------------------
5524
+ # Main logic
5525
+ # ----------------------------------------------------------------------
5526
+ # If there are portals, divide the wire into segments between portals,
5527
+ # then straighten each segment independently and reassemble.
5528
+ if portal_list:
5529
+ result_vertices = []
5530
+ sub_wires = _subdivide_by_portals()
5531
+
5532
+ for i, sub_wire in enumerate(sub_wires):
5533
+ if not Topology.IsInstance(sub_wire, "Wire"):
5534
+ continue
5535
+
5536
+ sub_verts = Topology.Vertices(sub_wire)
5537
+ if len(sub_verts) <= 2:
5538
+ straight_verts = sub_verts
5539
+ else:
5540
+ # IMPORTANT: recursive call with portals=[]
5541
+ straight_wire = Wire.Straighten(sub_wire, host=host,
5542
+ obstacles=obstacles,
5543
+ portals=[],
5544
+ tolerance=tolerance,
5545
+ silent=silent)
5546
+ if not Topology.IsInstance(straight_wire, "Wire"):
5547
+ straight_verts = sub_verts
5548
+ else:
5549
+ straight_verts = Topology.Vertices(straight_wire)
5550
+
5551
+ if not straight_verts:
5552
+ continue
5553
+
5554
+ # Avoid duplicate vertices between consecutive segments
5555
+ if not result_vertices:
5556
+ result_vertices.extend(straight_verts)
5557
+ else:
5558
+ result_vertices.extend(straight_verts[1:])
5559
+
5560
+ if len(result_vertices) < 2:
5561
+ # Fallback
5562
+ return wire
5563
+
5564
+ return Wire.ByVertices(result_vertices, close=False, silent=True)
5565
+
5566
+ # No portals: simple global straightening
5567
+ new_vertices = _straighten_vertices(vertices)
5568
+ if len(new_vertices) < 2:
5569
+ return wire
5570
+
5571
+ return Wire.ByVertices(new_vertices, close=False, silent=True)
5083
5572
 
5084
5573
  @staticmethod
5085
5574
  def Trapezoid(origin= None, widthA: float = 1.0, widthB: float = 0.75, offsetA: float = 0.0, offsetB: float = 0.0, length: float = 1.0, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
@@ -5440,6 +5929,105 @@ class Wire():
5440
5929
 
5441
5930
  return Wire.VertexByParameter(wire, u=compute_u(u))
5442
5931
 
5932
+
5933
+
5934
+ @staticmethod
5935
+ def ParameterAtVertex(wire, vertex, mantissa : int = 6, tolerance: float = 0.0001, silent: bool = False):
5936
+ """
5937
+ Returns the u-parameter of a vertex located on a manifold wire.
5938
+ u ranges from 0.0 (start) to 1.0 (end).
5939
+
5940
+ Parameters
5941
+ ----------
5942
+ wire : topologic_core.Wire
5943
+ The input wire.
5944
+ vertex : topologic_core.Vertex
5945
+ A vertex that lies somewhere on the wire.
5946
+ mantissa : int , optional
5947
+ The number of decimal places to round the result to. Default is 6.
5948
+ tolerance : float, optional
5949
+ Distance tolerance for matching the vertex to an edge. Default is 0.0001.
5950
+ silent : bool , optional
5951
+ If set to True, error and warning messages are suppressed. Default is False.
5952
+
5953
+ Returns
5954
+ -------
5955
+ float or None
5956
+ The global u-parameter ∈ [0, 1] of the vertex, or None on error.
5957
+ """
5958
+ from topologicpy.Topology import Topology
5959
+ from topologicpy.Vertex import Vertex
5960
+ from topologicpy.Edge import Edge
5961
+ from topologicpy.Wire import Wire
5962
+
5963
+ # --- Input validation ----------------------------------------------------
5964
+ if not Topology.IsInstance(wire, "Wire"):
5965
+ if not silent:
5966
+ print("Wire.ParameterAtVertex - Error: Input wire is not a valid wire. Returning None.")
5967
+ return None
5968
+
5969
+ if not Topology.IsInstance(vertex, "Vertex"):
5970
+ if not silent:
5971
+ print("Wire.ParameterAtVertex - Error: Input vertex is not a valid wertex. Returning None.")
5972
+ return None
5973
+
5974
+ if not Wire.IsManifold(wire):
5975
+ if not silent:
5976
+ print("Wire.ParameterAtVertex - Error: Input wire is non-manifold. Returning None.")
5977
+ return None
5978
+
5979
+ # --- Prepare wire edges ---------------------------------------------------
5980
+ edges = Wire.Edges(wire)
5981
+ if not edges:
5982
+ if not silent:
5983
+ print("Wire.ParameterAtVertex - Error: Wire has no edges. Returning None.")
5984
+ return None
5985
+
5986
+ edge_lengths = [Edge.Length(e) for e in edges]
5987
+ total_length = sum(edge_lengths)
5988
+ if total_length == 0:
5989
+ if not silent:
5990
+ print("Wire.ParameterAtVertex - Error: Wire has zero length. Returning None.")
5991
+ return None
5992
+
5993
+ # --- Special cases: endpoint vertices ------------------------------------
5994
+ if Vertex.Distance(vertex, Wire.StartVertex(wire)) <= tolerance:
5995
+ return 0.0
5996
+ if Vertex.Distance(vertex, Wire.EndVertex(wire)) <= tolerance:
5997
+ return 1.0
5998
+
5999
+ # --- Locate the edge containing the vertex -------------------------------
6000
+ accumulated = 0.0
6001
+
6002
+ for edge, e_length in zip(edges, edge_lengths):
6003
+
6004
+ # Check if vertex lies on this edge
6005
+ d = Vertex.Distance(vertex, edge)
6006
+ if d <= tolerance:
6007
+
6008
+ # Compute local parameter on this edge
6009
+ sv = Edge.StartVertex(edge)
6010
+ ev = Edge.EndVertex(edge)
6011
+
6012
+ # Local distances
6013
+ dist_sv = Vertex.Distance(sv, vertex)
6014
+ dist_ev = Vertex.Distance(ev, vertex)
6015
+
6016
+ if dist_sv + dist_ev == 0:
6017
+ local_u = 0.0
6018
+ else:
6019
+ local_u = dist_sv / (dist_sv + dist_ev)
6020
+
6021
+ # Global parameter u
6022
+ global_u = (accumulated + local_u * e_length) / total_length
6023
+ return round(global_u, mantissa)
6024
+
6025
+ accumulated += e_length
6026
+
6027
+ if not silent:
6028
+ print("Wire.ParameterAtVertex - Error: Vertex does not appear to lie on the wire. Returning None.")
6029
+ return None
6030
+
5443
6031
  @staticmethod
5444
6032
  def VertexByParameter(wire, u: float = 0):
5445
6033
  """
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.8.91'
1
+ __version__ = '0.8.93'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topologicpy
3
- Version: 0.8.91
3
+ Version: 0.8.93
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
@@ -2,7 +2,7 @@ topologicpy/ANN.py,sha256=gpflv4lFypOW789vO7mSkMLaMF_ZftVOCqCvtGr6-JA,47873
2
2
  topologicpy/Aperture.py,sha256=wNn5miB_IrGCBYuQ18HXQYRva20dUC3id4AJCulL7to,2723
3
3
  topologicpy/BVH.py,sha256=ts0Ru24ILjjfHa54SYNhMc8Jkyxwej1DV0Jv7P_6BoU,22513
4
4
  topologicpy/CSG.py,sha256=09la1-xzS9vr-WnV7tpJ0I-mkZ-XY0MRSd5iB50Nfgw,15556
5
- topologicpy/Cell.py,sha256=EHDCod7OJyiSw2oI7ttlO46bDET9N6RaQsPZLF7LP2k,198779
5
+ topologicpy/Cell.py,sha256=8GGeRJDoWV2qr__x41axOTIqtkep3U3VXRzM3qsQpPA,198783
6
6
  topologicpy/CellComplex.py,sha256=B8bAW6M5fClfXb9nSLDhrgtNRlU888Z4EcUzBZtBqss,68558
7
7
  topologicpy/Cluster.py,sha256=Vi5qn9dc9FRdZk1X5KrrU5YS8r4YDDA19C_nKo1IfA0,63725
8
8
  topologicpy/Color.py,sha256=hzSmgBWhiuYc55RSipkQNIgGtgyhC5BqY8AakNYEK-U,24486
@@ -26,14 +26,14 @@ topologicpy/ShapeGrammar.py,sha256=q_BvMKOBDW3GVSRjPLIGAZkHW2egw3mTOPzIyEpYOLg,2
26
26
  topologicpy/Shell.py,sha256=2EPzDT_t0IAjBRYPDuKNAz_Ax_HaEkvNpXBxDkPdcTg,101084
27
27
  topologicpy/Speckle.py,sha256=-eiTqJugd7pHiHpD3pDUcDO6CGhVyPV14HFRzaqEoaw,18187
28
28
  topologicpy/Sun.py,sha256=ezisiHfc2nd7A_8w0Ykq2VgbS0A9WNSg-tBwvfTQAVM,36735
29
- topologicpy/Topology.py,sha256=E_AyPPCIx_Eq-UT74QS3LKFXIwdwekRjJJGTo1CRMRY,548577
29
+ topologicpy/Topology.py,sha256=B8Ifgv2rWlfhjzoHeuNOofxML_zEIFDuc-nXUVQHV8g,549927
30
30
  topologicpy/Vector.py,sha256=pEC8YY3TeHGfGdeNgvdHjgMDwxGabp5aWjwYC1HSvMk,42236
31
31
  topologicpy/Vertex.py,sha256=26TrlX9OCZUN-lMlZG3g4RHTWBqw69NW4AOEgRz_YMo,91269
32
- topologicpy/Wire.py,sha256=Rhqw0CGEWIMVL1ICQqkCp9G-VnhhHLhEiDDR00fAn_s,248919
32
+ topologicpy/Wire.py,sha256=OzMTI5vxJ8XJPgwDAB31666Dz2Sp7H_U9oCnm81gPQA,272312
33
33
  topologicpy/__init__.py,sha256=RMftibjgAnHB1vdL-muo71RwMS4972JCxHuRHOlU428,928
34
- topologicpy/version.py,sha256=Pd3D7UyrHnkE76qM_AOq2A2ifXXq1OxDwEg8Q1gGwk8,23
35
- topologicpy-0.8.91.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
36
- topologicpy-0.8.91.dist-info/METADATA,sha256=S_NaUxOVkvDCJJ4xVVdtRSLZnkW0tzPwic19V2FLkuw,10535
37
- topologicpy-0.8.91.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- topologicpy-0.8.91.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
39
- topologicpy-0.8.91.dist-info/RECORD,,
34
+ topologicpy/version.py,sha256=RdWx3Y_QmIJKCA6l7kMMEkyg_LsudVQdkeSqFo0Zk8o,23
35
+ topologicpy-0.8.93.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
36
+ topologicpy-0.8.93.dist-info/METADATA,sha256=Pt_CPB53UTwU0aiDbVU9mUhHldLyJS4ir40Aj9vylZI,10535
37
+ topologicpy-0.8.93.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ topologicpy-0.8.93.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
39
+ topologicpy-0.8.93.dist-info/RECORD,,