topologicpy 0.8.88__py3-none-any.whl → 0.8.90__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/Graph.py CHANGED
@@ -4756,12 +4756,13 @@ class Graph:
4756
4756
  include: list = ["contains", "coveredBy", "covers", "crosses", "disjoint", "equals", "overlaps", "touches","within", "proximity"],
4757
4757
  proximityValues = [1, 5, 10],
4758
4758
  proximityLabels = ["near", "intermediate", "far"],
4759
- useInternalVertex = False,
4760
- vertexIDKey = "id",
4761
- edgeKeyFwd = "relFwd",
4762
- edgeKeyBwd = "relBwd",
4763
- connectsKey = "connects",
4764
- storeBREP = False,
4759
+ useShortestDistance: bool = False,
4760
+ useInternalVertex: bool = False,
4761
+ vertexIDKey: str = "id",
4762
+ edgeKeyFwd: str = "relFwd",
4763
+ edgeKeyBwd: str = "relBwd",
4764
+ connectsKey:str = "connects",
4765
+ storeBREP: bool = False,
4765
4766
  mantissa: int = 6,
4766
4767
  tolerance: float = 0.0001,
4767
4768
  silent: bool = False
@@ -4785,7 +4786,8 @@ class Graph:
4785
4786
  proximityLabels: list , optional
4786
4787
  The list of range labels (e.g. "near", "intermediate", "far") that correspond to the proximityValues list.
4787
4788
  The list must have the same number of elements as the proximityValues list. Default is ["near", "intermediate", "far"]
4788
-
4789
+ useShortestDistance: bool , optional
4790
+ If set to True, the shortest distance between objects is used. Otherwise, the distance between their centroids is used. Default is False.
4789
4791
  useInternalVertex: bool , optional
4790
4792
  If set to True, an internal vertex of the represented topology will be used as a graph node.
4791
4793
  Otherwise, its centroid will be used. Default is False.
@@ -4814,6 +4816,7 @@ class Graph:
4814
4816
  """
4815
4817
  from topologicpy.Graph import Graph
4816
4818
  from topologicpy.BVH import BVH
4819
+ from topologicpy.Cell import Cell
4817
4820
  from topologicpy.Vertex import Vertex
4818
4821
  from topologicpy.Topology import Topology
4819
4822
  from topologicpy.Dictionary import Dictionary
@@ -4850,6 +4853,234 @@ class Graph:
4850
4853
  v = Topology.SetDictionary(v, Topology.Dictionary(topologyList[0]))
4851
4854
  return Graph.ByVerticesEdges([v], [])
4852
4855
 
4856
+ # ---------- Calculate Proximity ------------
4857
+
4858
+ def _calc_proximity(topologies: list,
4859
+ ranges: list,
4860
+ labels: list,
4861
+ useShortestDistance: bool = True,
4862
+ tolerance: float = 0.0001,
4863
+ silent: bool = False):
4864
+ """
4865
+ Creates a proximity graph from a list of topologies using a BVH
4866
+ to prune distance checks for large input sets.
4867
+
4868
+ Each topology is represented by a vertex in the graph.
4869
+ An edge is created between two vertices if the distance between
4870
+ their corresponding topologies is <= max(ranges).
4871
+
4872
+ A BVH is used as a broad-phase accelerator: for each topology,
4873
+ a query box centred at its centroid and sized to cover the
4874
+ maximum proximity range is used to retrieve only nearby
4875
+ candidates. Exact distances are then computed only for those
4876
+ candidates.
4877
+
4878
+ Parameters
4879
+ ----------
4880
+ topologies : list of topologic_core.Topology
4881
+ The input topologies to be represented as vertices.
4882
+ ranges : list of float
4883
+ A list of positive numeric thresholds (e.g. [1.0, 3.0, 5.0]).
4884
+ Interpreted as upper bounds of proximity bands.
4885
+ Distances larger than max(ranges) are ignored (no edge).
4886
+ labels : list of str
4887
+ A list of proximity labels, same length as `ranges`.
4888
+ For a pair distance d:
4889
+ - d <= ranges[0] -> labels[0]
4890
+ - ranges[0] < d <= ranges[1] -> labels[1]
4891
+ - ...
4892
+ useShortestDistance : bool , optional
4893
+ If True, use Topology.ShortestDistance(topologyA, topologyB)
4894
+ if available. If False (or if that fails), fall back to the
4895
+ distance between the centroids of the two topologies.
4896
+ Default is True.
4897
+ tolerance : float , optional
4898
+ A small numeric tolerance used when comparing distances to
4899
+ range bounds. Default is 0.0001.
4900
+ silent : bool , optional
4901
+ If False, basic sanity-check warnings are printed.
4902
+ Default is False.
4903
+
4904
+ Returns
4905
+ -------
4906
+ graph : topologic_core.Graph
4907
+ A graph whose vertices correspond to the input topologies
4908
+ and whose edges connect topologies that fall within the
4909
+ supplied distance ranges. Each edge dictionary contains:
4910
+ - "distance" : float (actual distance)
4911
+ - "proximity" : str (label from `labels`)
4912
+ - "range_max" : float (upper bound used for the bin)
4913
+ - "source_index" : int
4914
+ - "target_index" : int
4915
+
4916
+ Notes
4917
+ -----
4918
+ - Complexity is approximately O(n log n + k) where k is the
4919
+ number of candidate pairs returned by the BVH.
4920
+ - BVH is used only as a broad-phase filter; exact distance
4921
+ tests still guarantee correctness with respect to `ranges`.
4922
+ """
4923
+
4924
+ # Basic validation
4925
+ if not isinstance(topologies, list) or len(topologies) < 2:
4926
+ if not silent:
4927
+ print("Graph.BySpatialRelationships - Error: Need a list of at least two topologies.")
4928
+ return None
4929
+
4930
+ if not isinstance(ranges, list) or not isinstance(labels, list):
4931
+ if not silent:
4932
+ print("Graph.BySpatialRelationships - Error: 'proximityValues' and 'proximityLabels' must be lists.")
4933
+ return None
4934
+
4935
+ if len(ranges) == 0 or len(ranges) != len(labels):
4936
+ if not silent:
4937
+ print("Graph.BySpatialRelationships - Error: 'proximityValues' must be non-empty and "
4938
+ "have the same length as 'labels'.")
4939
+ return None
4940
+
4941
+ # Sort ranges and labels together (ascending by range)
4942
+ try:
4943
+ rl = sorted(zip(ranges, labels), key=lambda x: x[0])
4944
+ except Exception:
4945
+ if not silent:
4946
+ print("Graph.BySpatialRelationships - Error: Could not sort ranges; check they are numeric.")
4947
+ return None
4948
+
4949
+ sorted_ranges = [r for (r, _) in rl]
4950
+ sorted_labels = [lab for (_, lab) in rl]
4951
+
4952
+ max_range = sorted_ranges[-1]
4953
+
4954
+ # Precompute representative vertices (centroids) for each topology
4955
+ vertices = []
4956
+ n = len(topologies)
4957
+ for i, topo in enumerate(topologies):
4958
+ if not topo:
4959
+ if not silent:
4960
+ print(f"Graph.BySpatialRelationships - Warning: Ignoring None topology at index {i}.")
4961
+ vertices.append(None)
4962
+ continue
4963
+ try:
4964
+ c_vtx = Topology.Centroid(topo)
4965
+ except Exception:
4966
+ # Fallback if centroid fails
4967
+ if not silent:
4968
+ print(f"Graph.BySpatialRelationships - Error: Failed to compute centroid for topology {i}, "
4969
+ f"using origin as placeholder.")
4970
+ c_vtx = Vertex.ByCoordinates(0, 0, 0)
4971
+
4972
+ # Attach index dictionary to the vertex (not to the original topology)
4973
+ d_keys = ["index"]
4974
+ d_vals = [i]
4975
+ v_dict = Dictionary.ByKeysValues(d_keys, d_vals)
4976
+ c_vtx = Topology.SetDictionary(c_vtx, v_dict)
4977
+ vertices.append(c_vtx)
4978
+
4979
+ # Build BVH on the original topologies
4980
+ try:
4981
+ bvh = BVH.ByTopologies(topologies)
4982
+ except Exception as e:
4983
+ if not silent:
4984
+ print(f"Graph.BySpatialRelationships - Error: Failed to build BVH, falling back to O(n^2): {e}")
4985
+ # Fallback: use the non-BVH variant if you like,
4986
+ # or just early-return None. Here we just bail out.
4987
+ return None
4988
+
4989
+ # Map from topology identity to index for fast lookup
4990
+ id_to_index = {id(topo): i for i, topo in enumerate(topologies)}
4991
+
4992
+ # Helper to compute distance between two topologies
4993
+ def _distance(topoA, topoB, vA, vB):
4994
+ d_val = None
4995
+ if useShortestDistance:
4996
+ try:
4997
+ d_val = Topology.ShortestDistance(topoA, topoB)
4998
+ except Exception:
4999
+ d_val = None
5000
+ if d_val is None:
5001
+ try:
5002
+ d_val = Vertex.Distance(vA, vB)
5003
+ except Exception:
5004
+ d_val = None
5005
+ return d_val
5006
+
5007
+ edges = []
5008
+
5009
+ # Main loop: for each topology, query BVH for candidates within
5010
+ # a bounding box of size 2*max_range around its centroid.
5011
+ for i in range(n):
5012
+ topo_i = topologies[i]
5013
+ v_i = vertices[i]
5014
+ if topo_i is None or v_i is None:
5015
+ continue
5016
+
5017
+ # Build a query box centered at the centroid with size 2 * max_range
5018
+ try:
5019
+ query_box = Cell.Prism(
5020
+ origin=v_i,
5021
+ width=2 * max_range,
5022
+ length=2 * max_range,
5023
+ height=2 * max_range
5024
+ )
5025
+ except Exception as q_err:
5026
+ if not silent:
5027
+ print(f"Graph.BySpatialRelationships - Error: Failed to build query box for {i}: {q_err}")
5028
+ continue
5029
+
5030
+ try:
5031
+ candidates = BVH.Clashes(bvh, query_box)
5032
+ except Exception as c_err:
5033
+ if not silent:
5034
+ print(f"Graph.BySpatialRelationships - Error: BVH.Clashes failed for {i}: {c_err}")
5035
+ continue
5036
+
5037
+ if not candidates:
5038
+ continue
5039
+
5040
+ for cand in candidates:
5041
+ j = id_to_index.get(id(cand), None)
5042
+ if j is None:
5043
+ continue
5044
+ # Enforce i < j to avoid duplicate edges
5045
+ if j <= i:
5046
+ continue
5047
+
5048
+ topo_j = topologies[j]
5049
+ v_j = vertices[j]
5050
+ if topo_j is None or v_j is None:
5051
+ continue
5052
+
5053
+ # Compute exact distance
5054
+ d = _distance(topo_i, topo_j, v_i, v_j)
5055
+ if d is None:
5056
+ if not silent:
5057
+ print(f"Graph.BySpatialRelationships - Error: Could not compute distance between "
5058
+ f"{i} and {j}.")
5059
+ continue
5060
+
5061
+ # Skip if beyond max range (plus tolerance)
5062
+ if d > max_range + tolerance:
5063
+ continue
5064
+
5065
+ # Bin the distance into the appropriate range/label
5066
+ label = None
5067
+ range_max = None
5068
+ for r, lab in zip(sorted_ranges, sorted_labels):
5069
+ if d <= r + tolerance:
5070
+ label = lab
5071
+ range_max = r
5072
+ break
5073
+
5074
+ if label is None:
5075
+ continue
5076
+
5077
+ e_keys = ["distance", "proximity", "range_max", "source_index", "target_index"]
5078
+ e_values = [float(d), str(label), float(range_max), i, j]
5079
+ d = Dictionary.ByKeysValues(e_keys, e_values)
5080
+ edges.append(d)
5081
+
5082
+ return edges
5083
+
4853
5084
  # ---------- BVH once ----------
4854
5085
  bvh = BVH.ByTopologies(topologyList, silent=True)
4855
5086
 
@@ -4924,31 +5155,23 @@ class Graph:
4924
5155
  ))
4925
5156
 
4926
5157
  # ---------- main loops (each unordered pair once) ----------
4927
- used = []
4928
- proximity_edges = []
5158
+ if "proximity" in include:
5159
+ prox_dicts = _calc_proximity(topologies = topologyList,
5160
+ ranges = proximityValues,
5161
+ labels = proximityLabels,
5162
+ useShortestDistance = useShortestDistance,
5163
+ tolerance = tolerance,
5164
+ silent = silent)
5165
+ for prox_dict in prox_dicts:
5166
+ ai = Dictionary.ValueAtKey(prox_dict, "source_index")
5167
+ bj = Dictionary.ValueAtKey(prox_dict, "target_index")
5168
+ rel = Dictionary.ValueAtKey(prox_dict, "proximity")
5169
+ if (rel in proximityLabels):
5170
+ _add_edge(ai, bj, rel)
5171
+
4929
5172
  for i, a in enumerate(topologyList):
4930
5173
  candidates = []
4931
5174
  ai = i
4932
- if "proximity" in include:
4933
- for j, b in enumerate(topologyList):
4934
- bj = index_of.get(id(b))
4935
- if i == bj or (i,bj) in used or (bj,i) in used:
4936
- continue
4937
- else:
4938
- used.append((i,bj))
4939
- used.append((bj,i))
4940
- rel = Topology.SpatialRelationship( a,
4941
- b,
4942
- include=["proximity"],
4943
- proximityValues = proximityValues,
4944
- proximityLabels = proximityLabels,
4945
- mantissa=mantissa,
4946
- tolerance=tolerance,
4947
- silent=True
4948
- )
4949
- rel_ok = (rel in proximityLabels)
4950
- if rel_ok:
4951
- _add_edge(ai, bj, rel)
4952
5175
  candidates = BVH.Clashes(bvh, a) or []
4953
5176
  if not candidates:
4954
5177
  # If you want to connect "disjoint" to *all* non-candidates, that would be O(n) per i.
@@ -5028,7 +5251,8 @@ class Graph:
5028
5251
  useInternalVertex: bool = False,
5029
5252
  storeBREP: bool =False,
5030
5253
  mantissa: int = 6,
5031
- tolerance: float = 0.0001):
5254
+ tolerance: float = 0.0001,
5255
+ silent: float = False):
5032
5256
  """
5033
5257
  Creates a graph.See https://en.wikipedia.org/wiki/Graph_(discrete_mathematics).
5034
5258
 
@@ -5082,6 +5306,8 @@ class Graph:
5082
5306
  If set to True, store the BRep of the subtopology in its representative vertex. Default is False.
5083
5307
  tolerance : float , optional
5084
5308
  The desired tolerance. Default is 0.0001.
5309
+ silent : bool , optional
5310
+ If set to True, error and warning messages are suppressed. Default is False.
5085
5311
 
5086
5312
  Returns
5087
5313
  -------
@@ -5096,6 +5322,10 @@ class Graph:
5096
5322
  from topologicpy.Topology import Topology
5097
5323
  from topologicpy.Aperture import Aperture
5098
5324
 
5325
+ if not Topology.IsInstance(topology, "topology"):
5326
+ if not silent:
5327
+ print("Graph.ByTopology - Error: The input topology parameter is not a valid topology. Returning None.")
5328
+ return None
5099
5329
  def _viaSharedTopologies(vt, sharedTops):
5100
5330
  verts = []
5101
5331
  eds = []
@@ -15354,7 +15584,7 @@ class Graph:
15354
15584
  try:
15355
15585
  gsv = Graph.NearestVertex(graph, vertexA)
15356
15586
  gev = Graph.NearestVertex(graph, vertexB)
15357
- shortest_path = graph.ShortestPath(gsv, gev, vertexKey, edgeKey)
15587
+ shortest_path = graph.ShortestPath(gsv, gev, vertexKey, edgeKey) # Hook to Core
15358
15588
  if not shortest_path == None:
15359
15589
  if Topology.IsInstance(shortest_path, "Edge"):
15360
15590
  shortest_path = Wire.ByEdges([shortest_path])
@@ -15367,6 +15597,70 @@ class Graph:
15367
15597
  except:
15368
15598
  return None
15369
15599
 
15600
+ @staticmethod
15601
+ def shortestPathInFace(graph, face, vertexA, vertexB, mode: int = 0, meshSize: float = None, tolerance: float = 0.0001, silent: bool = False):
15602
+ """
15603
+ Returns the shortest path that connects the input vertices.
15604
+
15605
+ Parameters
15606
+ ----------
15607
+ face : topologic_core.Face
15608
+ The input face. This is assumed to be planar and resting on the XY plane (z = 0)
15609
+ vertexA : topologic_core.Vertex
15610
+ The first input vertex.
15611
+ vertexB : topologic_core.Vertex
15612
+ The second input vertex.
15613
+ mode : int , optional
15614
+ The desired mode of meshing algorithm. Several options are available:
15615
+ 0: Classic
15616
+ 1: MeshAdapt
15617
+ 3: Initial Mesh Only
15618
+ 5: Delaunay
15619
+ 6: Frontal-Delaunay
15620
+ 7: BAMG
15621
+ 8: Fontal-Delaunay for Quads
15622
+ 9: Packing of Parallelograms
15623
+ All options other than 0 (Classic) use the gmsh library. See https://gmsh.info/doc/texinfo/gmsh.html#Mesh-options
15624
+ WARNING: The options that use gmsh can be very time consuming and can create very heavy geometry.
15625
+ meshSize : float , optional
15626
+ The desired size of the mesh when using the "mesh" option. If set to None, it will be
15627
+ calculated automatically and set to 10% of the overall size of the face.
15628
+ tolerance : float , optional
15629
+ The desired tolerance. Default is 0.0001.
15630
+ silent : bool , optional
15631
+ If set to True, error and warning messages are suppressed. Default is False.
15632
+
15633
+ Returns
15634
+ -------
15635
+ topologic_core.Wire
15636
+ The shortest path between the input vertices.
15637
+
15638
+ """
15639
+ from topologicpy.Wire import Wire
15640
+ from topologicpy.Topology import Topology
15641
+
15642
+ if not Topology.IsInstance(face, "face"):
15643
+ if not silent:
15644
+ print("Face.ShortestPath - Error: The input face parameter is not a topologic face. Returning None.")
15645
+ return None
15646
+ if not Topology.IsInstance(vertexA, "vertex"):
15647
+ if not silent:
15648
+ print("Face.ShortestPath - Error: The input vertexA parameter is not a topologic vertex. Returning None.")
15649
+ return None
15650
+ if not Topology.IsInstance(vertexB, "vertex"):
15651
+ if not silent:
15652
+ print("Face.ShortestPath - Error: The input vertexB parameter is not a topologic vertex. Returning None.")
15653
+ return None
15654
+
15655
+ sp = Graph.ShortestPath(graph, vertexA, vertexB)
15656
+ if sp == None:
15657
+ if not silent:
15658
+ print("Face.ShortestPath - Error: Could not find the shortest path. Returning None.")
15659
+ return None
15660
+
15661
+ new_path = Wire.StraightenInFace(sp, face, tolerance = tolerance)
15662
+ return new_path
15663
+
15370
15664
  @staticmethod
15371
15665
  def ShortestPaths(graph, vertexA, vertexB, vertexKey="", edgeKey="length", timeLimit=10,
15372
15666
  pathLimit=10, tolerance=0.0001):
topologicpy/Wire.py CHANGED
@@ -331,16 +331,19 @@ class Wire():
331
331
 
332
332
  else:
333
333
  best_br = boundingRectangle
334
- x_min, y_min, maxX, maxY = best_br
334
+ x_min, y_min, x_max, y_max = best_br
335
335
  vb1 = Vertex.ByCoordinates(x_min, y_min, 0)
336
- vb2 = Vertex.ByCoordinates(maxX, y_min, 0)
337
- vb3 = Vertex.ByCoordinates(maxX, maxY, 0)
338
- vb4 = Vertex.ByCoordinates(x_min, maxY, 0)
336
+ vb2 = Vertex.ByCoordinates(x_max, y_min, 0)
337
+ vb3 = Vertex.ByCoordinates(x_max, y_max, 0)
338
+ vb4 = Vertex.ByCoordinates(x_min, y_max, 0)
339
339
 
340
340
  boundingRectangle = Wire.ByVertices([vb1, vb2, vb3, vb4], close=True, tolerance=tolerance, silent=silent)
341
341
  boundingRectangle = Topology.Rotate(boundingRectangle, origin=origin, axis=[0, 0, 1], angle=-best_z)
342
342
  boundingRectangle = Topology.Unflatten(boundingRectangle, origin=f_origin, direction=normal)
343
- dictionary = Dictionary.ByKeysValues(["zrot"], [best_z])
343
+ dictionary = Dictionary.ByKeysValues(["zrot", "xmin", "ymin", "xmax", "ymax", "width", "length"],
344
+ [best_z, x_min, y_min, x_max, y_max, (x_max - x_min), (y_max - y_min)])
345
+
346
+ #dictionary = Dictionary.ByKeysValues(["zrot"], [best_z])
344
347
  boundingRectangle = Topology.SetDictionary(boundingRectangle, dictionary)
345
348
  return boundingRectangle
346
349
 
@@ -2426,7 +2429,254 @@ class Wire():
2426
2429
  # Unflatten the wire
2427
2430
  return_wire = Topology.Unflatten(flat_wire, origin=Vertex.Origin(), direction=normal)
2428
2431
  return return_wire
2429
-
2432
+
2433
+ @staticmethod
2434
+ def Funnel(face,
2435
+ vertexA,
2436
+ vertexB,
2437
+ portals,
2438
+ tolerance: float = 0.0001,
2439
+ silent: float = False):
2440
+ """
2441
+ Returns a Wire representing a smoothed path inside the given face using
2442
+ the funnel (string-pulling) algorithm.
2443
+
2444
+ The algorithm assumes that a corridor has already been computed, and is
2445
+ provided as an ordered list of "portals" (pairs of vertices) that lie
2446
+ on the face between the start and end locations.
2447
+
2448
+ Parameters
2449
+ ----------
2450
+ face : topologic_core.Face
2451
+ The planar face on which navigation occurs. All vertices must lie
2452
+ on this face.
2453
+ vertexA : topologic_core.Vertex
2454
+ The start point of the path.
2455
+ vertexB : topologic_core.Vertex
2456
+ The end point of the path.
2457
+ portals : list of tuple(Vertex, Vertex)
2458
+ Ordered list of corridor edges. Each item is (leftVertex, rightVertex)
2459
+ describing the visible "portal" between two consecutive regions along
2460
+ the navmesh path.
2461
+ tolerance : float , optional
2462
+ Numerical tolerance used when comparing orientations and distances.
2463
+ Default is 0.0001.
2464
+ silent : bool , optional
2465
+ If set to True, error and warning messages are suppressed. Default is False.
2466
+
2467
+ Returns
2468
+ -------
2469
+ wire : topologic_core.Wire
2470
+ A Wire representing the smoothed path from startVertex to endVertex
2471
+ that stays inside the navigation corridor on the face.
2472
+ """
2473
+ from topologicpy.Dictionary import Dictionary
2474
+ from topologicpy.Vertex import Vertex
2475
+ from topologicpy.Face import Face
2476
+ from topologicpy.Topology import Topology
2477
+
2478
+ if not Topology.IsInstance(face, "face"):
2479
+ if not silent:
2480
+ print("Wire.Funnel - Error: The input face parameter is not a topologic face. Returning None.")
2481
+ return None
2482
+ if not Topology.IsInstance(vertexA, "vertex"):
2483
+ if not silent:
2484
+ print("Wire.Funnel - Error: The input vertexA parameter is not a topologic vertex. Returning None.")
2485
+ return None
2486
+ if not Topology.IsInstance(vertexB, "vertex"):
2487
+ if not silent:
2488
+ print("Wire.Funnel - Error: The input vertexB parameter is not a topologic vertex. Returning None.")
2489
+ return None
2490
+
2491
+ # ------------------------------------------------------------
2492
+ # 1. Basic helpers
2493
+ # ------------------------------------------------------------
2494
+ def _norm(v):
2495
+ return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
2496
+
2497
+ def _normalize(v):
2498
+ n = _norm(v)
2499
+ if n < tolerance:
2500
+ return (0.0, 0.0, 0.0)
2501
+ return (v[0] / n, v[1] / n, v[2] / n)
2502
+
2503
+ def _dot(a, b):
2504
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
2505
+
2506
+ def _cross(a, b):
2507
+ return (
2508
+ a[1] * b[2] - a[2] * b[1],
2509
+ a[2] * b[0] - a[0] * b[2],
2510
+ a[0] * b[1] - a[1] * b[0],
2511
+ )
2512
+
2513
+ def _sub(a, b):
2514
+ return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
2515
+
2516
+ def _tri_area2(a2, b2, c2):
2517
+ """
2518
+ Twice the signed area of triangle (a, b, c) in 2D.
2519
+ Positive => c is to the left of ab
2520
+ Negative => c is to the right of ab
2521
+ """
2522
+ return (b2[0] - a2[0]) * (c2[1] - a2[1]) - (b2[1] - a2[1]) * (c2[0] - a2[0])
2523
+
2524
+ def _coords3d(v):
2525
+ x, y, z = Vertex.Coordinates(v)
2526
+ return (x, y, z)
2527
+
2528
+ # ------------------------------------------------------------
2529
+ # 2. Build a local 2D coordinate system on the face
2530
+ # ------------------------------------------------------------
2531
+ # Face normal
2532
+ n_vec = Face.Normal(face) # [nx, ny, nz]
2533
+ n = _normalize((n_vec[0], n_vec[1], n_vec[2]))
2534
+
2535
+ # Choose an arbitrary vector not parallel to n
2536
+ if abs(n[0]) < 0.9:
2537
+ arbitrary = (1.0, 0.0, 0.0)
2538
+ else:
2539
+ arbitrary = (0.0, 1.0, 0.0)
2540
+
2541
+ u = _normalize(_cross(n, arbitrary)) # tangent
2542
+ v = _cross(n, u) # bitangent, already orthogonal and normalized
2543
+
2544
+ def _project_to_2d(vertex):
2545
+ p = _coords3d(vertex)
2546
+ # project onto basis (u, v)
2547
+ return (_dot(p, u), _dot(p, v))
2548
+
2549
+ # Precompute 2D coords for start, end and all portal vertices
2550
+ start2d = _project_to_2d(vertexA)
2551
+ end2d = _project_to_2d(vertexB)
2552
+
2553
+ portal2d = []
2554
+ for l_v, r_v in portals:
2555
+ portal2d.append((_project_to_2d(l_v), _project_to_2d(r_v)))
2556
+
2557
+ # ------------------------------------------------------------
2558
+ # 3. Funnel algorithm in 2D
2559
+ # (based on classic Recast / string-pulling implementation)
2560
+ # ------------------------------------------------------------
2561
+ path_vertices = [vertexA]
2562
+
2563
+ apex2d = start2d
2564
+ apexVertex = vertexA
2565
+ apexIndex = -1
2566
+
2567
+ left2d = start2d
2568
+ right2d = start2d
2569
+ leftVertex = vertexA
2570
+ rightVertex = vertexB
2571
+ leftIndex = -1
2572
+ rightIndex = -1
2573
+
2574
+ n_portals = len(portals)
2575
+ i = 0
2576
+
2577
+ # We will process all portals, and then a final "portal" at the goal (end, end)
2578
+ while i <= n_portals:
2579
+ if i < n_portals:
2580
+ newLeft2d, newRight2d = portal2d[i]
2581
+ newLeftVertex, newRightVertex = portals[i]
2582
+ else:
2583
+ # last "portal" is the goal point itself
2584
+ newLeft2d = end2d
2585
+ newRight2d = end2d
2586
+ newLeftVertex = vertexB
2587
+ newRightVertex = vertexB
2588
+
2589
+ # --------------------------------------------------------
2590
+ # Update right side of funnel
2591
+ # --------------------------------------------------------
2592
+ area_apex_right_newRight = _tri_area2(apex2d, right2d, newRight2d)
2593
+ if area_apex_right_newRight <= tolerance:
2594
+ # New right vertex is "inside" or tightening the funnel
2595
+ area_apex_left_newRight = _tri_area2(apex2d, left2d, newRight2d)
2596
+ if (apexVertex == rightVertex) or (area_apex_left_newRight > tolerance):
2597
+ # Tighten the funnel on the right side
2598
+ right2d = newRight2d
2599
+ rightVertex = newRightVertex
2600
+ rightIndex = i
2601
+ else:
2602
+ # Right over left, so left becomes the new apex
2603
+ path_vertices.append(leftVertex)
2604
+ apex2d = _project_to_2d(leftVertex)
2605
+ apexVertex = leftVertex
2606
+ apexIndex = leftIndex
2607
+
2608
+ # Reset funnel
2609
+ left2d = apex2d
2610
+ right2d = apex2d
2611
+ leftVertex = apexVertex
2612
+ rightVertex = apexVertex
2613
+ leftIndex = apexIndex
2614
+ rightIndex = apexIndex
2615
+
2616
+ # Restart from the new apex
2617
+ i = apexIndex + 1
2618
+ continue
2619
+
2620
+ # --------------------------------------------------------
2621
+ # Update left side of funnel
2622
+ # --------------------------------------------------------
2623
+ area_apex_left_newLeft = _tri_area2(apex2d, left2d, newLeft2d)
2624
+ if area_apex_left_newLeft >= -tolerance:
2625
+ # New left vertex is "inside" or tightening the funnel
2626
+ area_apex_right_newLeft = _tri_area2(apex2d, right2d, newLeft2d)
2627
+ if (apexVertex == leftVertex) or (area_apex_right_newLeft < -tolerance):
2628
+ # Tighten funnel on the left side
2629
+ left2d = newLeft2d
2630
+ leftVertex = newLeftVertex
2631
+ leftIndex = i
2632
+ else:
2633
+ # Left over right, so right becomes the new apex
2634
+ path_vertices.append(rightVertex)
2635
+ apex2d = _project_to_2d(rightVertex)
2636
+ apexVertex = rightVertex
2637
+ apexIndex = rightIndex
2638
+
2639
+ # Reset funnel
2640
+ left2d = apex2d
2641
+ right2d = apex2d
2642
+ leftVertex = apexVertex
2643
+ rightVertex = apexVertex
2644
+ leftIndex = apexIndex
2645
+ rightIndex = apexIndex
2646
+
2647
+ # Restart from the new apex
2648
+ i = apexIndex + 1
2649
+ continue
2650
+
2651
+ i += 1
2652
+
2653
+ # Finally, add the end point if it is not already in the path
2654
+ if path_vertices[-1] is not vertexB:
2655
+ path_vertices.append(vertexB)
2656
+
2657
+ # ------------------------------------------------------------
2658
+ # 4. Build and return the Topologic wire
2659
+ # ------------------------------------------------------------
2660
+ return_wire = Wire.ByVertices(path_vertices, close=False, silent=True)
2661
+ bb = Wire.BoundingRectangle(face)
2662
+ d = Topology.Dictionary(bb)
2663
+ width = Dictionary.ValueAtKey(d, "width")
2664
+ length = Dictionary.ValueAtKey(d, "length")
2665
+ size = max(width, length)
2666
+ percentage = 0.25 # Start with 25% of the total size
2667
+ is_ok = False
2668
+ while is_ok == False and percentage > 0:
2669
+ new_wire = Wire.Simplify(return_wire, tolerance=size*percentage, silent=True)
2670
+ test_wire = Topology.Scale(new_wire, Topology.Centroid(new_wire), 0.95, 0.95, 1)
2671
+ result = Topology.Difference(test_wire, face, tolerance=tolerance, silent=True)
2672
+ if result is None:
2673
+ is_ok = True
2674
+ return_wire = new_wire
2675
+ percentage -= 0.01
2676
+ print("Wire.Funnel - Result:", result)
2677
+ print("Wire.Funnel - Percentage:", percentage)
2678
+ return new_wire
2679
+
2430
2680
  @staticmethod
2431
2681
  def InteriorAngles(wire, tolerance: float = 0.0001, mantissa: int = 6) -> list:
2432
2682
  """
@@ -4188,7 +4438,7 @@ class Wire():
4188
4438
  if not silent:
4189
4439
  print("Wire.Simplify - Warning: Could not generate enough vertices for a simplified wire. Returning the original wire.")
4190
4440
  wire
4191
- new_wire = Wire.ByVertices(new_vertices, close=Wire.IsClosed(wire), tolerance=tolerance)
4441
+ new_wire = Wire.ByVertices(new_vertices, close=Wire.IsClosed(wire), tolerance=tolerance, silent=True)
4192
4442
  if not Topology.IsInstance(new_wire, "wire"):
4193
4443
  if not silent:
4194
4444
  print("Wire.Simplify - Warning: Could not generate a simplified wire. Returning the original wire.")
@@ -4713,6 +4963,124 @@ class Wire():
4713
4963
  sv, ev = Wire.StartEndVertices(wire, silent=silent)
4714
4964
  return sv
4715
4965
 
4966
+ @staticmethod
4967
+ def StraightenInFace(wire, face, tolerance: float = 0.0001):
4968
+ """
4969
+ 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.
4983
+
4984
+ Parameters
4985
+ ----------
4986
+ wire : topologic_core.Wire
4987
+ The input path wire whose vertices define the route to be
4988
+ straightened.
4989
+ face : topologic_core.Face
4990
+ The face within which the straightened edges must lie.
4991
+ 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.
4995
+
4996
+ Returns
4997
+ -------
4998
+ wire : topologic_core.Wire
4999
+ A new Wire whose vertices define the recursively straightened path.
5000
+ """
5001
+ from topologicpy.Vertex import Vertex
5002
+ from topologicpy.Edge import Edge
5003
+ from topologicpy.Wire import Wire
5004
+ from topologicpy.Face import Face
5005
+ from topologicpy.Topology import Topology
5006
+ # Get ordered vertices of the wire
5007
+ vertices = Topology.Vertices(wire)
5008
+ n = len(vertices)
5009
+
5010
+ if n <= 2:
5011
+ # Nothing to straighten
5012
+ return wire
5013
+
5014
+ def _edge_inside_face(v_start, v_end):
5015
+ """
5016
+ 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.
5019
+ """
5020
+ if v_start is v_end:
5021
+ return True
5022
+ edge = Edge.ByStartVertexEndVertex(v_start, v_end)
5023
+ diff = Topology.Difference(edge, face)
5024
+ return diff is None
5025
+
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.
5031
+
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.
5034
+ """
5035
+ v_start = vertices[start_idx]
5036
+ best_j = None
5037
+
5038
+ for j in range(start_idx + 1, n):
5039
+ v_end = vertices[j]
5040
+ if _edge_inside_face(v_start, v_end):
5041
+ 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.
5044
+
5045
+ if best_j is None:
5046
+ # Fallback: use the immediate next vertex to avoid stalling
5047
+ best_j = min(start_idx + 1, n - 1)
5048
+
5049
+ return best_j
5050
+
5051
+ def _straighten_recursive(start_idx, out_vertices):
5052
+ """
5053
+ Recursive helper.
5054
+
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.
5058
+ """
5059
+ # Base case: we are at the last vertex
5060
+ if start_idx == n - 1:
5061
+ out_vertices.append(vertices[start_idx])
5062
+ return
5063
+
5064
+ # Find furthest valid index reachable from start_idx
5065
+ next_idx = _find_longest_valid_index(start_idx)
5066
+
5067
+ # Add the starting vertex for this segment
5068
+ out_vertices.append(vertices[start_idx])
5069
+
5070
+ # Recurse from the chosen furthest index
5071
+ _straighten_recursive(next_idx, out_vertices)
5072
+
5073
+ # Run the recursion
5074
+ new_vertices = []
5075
+ _straighten_recursive(0, new_vertices)
5076
+
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])
5080
+
5081
+ # Build the new straightened wire
5082
+ return Wire.ByVertices(new_vertices, close=False)
5083
+
4716
5084
  @staticmethod
4717
5085
  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):
4718
5086
  """
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.8.88'
1
+ __version__ = '0.8.90'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topologicpy
3
- Version: 0.8.88
3
+ Version: 0.8.90
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
@@ -12,7 +12,7 @@ topologicpy/Dictionary.py,sha256=goODXIM6AoC5Qn_d8LGc5pRoxZKgIWbkn3IOEbsQ4c4,449
12
12
  topologicpy/Edge.py,sha256=aiRd1xZgG2GGYHxva0bM-kDy3AVmwGA_S2pMur8EeMg,74911
13
13
  topologicpy/EnergyModel.py,sha256=MEai1GF1hINeH5bhclJj_lpMU3asFTvW2RlPm40GNj4,57794
14
14
  topologicpy/Face.py,sha256=qAl36LcwiyRclMM2pI9NyWHzmgNlaykXiJx1wu10RmA,201317
15
- topologicpy/Graph.py,sha256=oDzAX_lmM10ayRA8mNenlccJu-oe-FYwW2GHbAp9x5s,798255
15
+ topologicpy/Graph.py,sha256=c-n_9xKXizbyGGWBw62fECsqMo-UoqvQJz6nw82DWgA,811337
16
16
  topologicpy/Grid.py,sha256=3OsBMyHh4w8gpFOTMKHMNTpo62V0CwRNu5cwm87yDUA,18421
17
17
  topologicpy/Helper.py,sha256=NsmMlbbKFPRX6jfoko-ZQVQ7MBsfVp9FD0ZvC2U7q-8,32002
18
18
  topologicpy/Honeybee.py,sha256=dBk01jIvxjQMGHqSarM1Cukv16ot4Op7Dwlitn2OMoc,48990
@@ -29,11 +29,11 @@ topologicpy/Sun.py,sha256=ezisiHfc2nd7A_8w0Ykq2VgbS0A9WNSg-tBwvfTQAVM,36735
29
29
  topologicpy/Topology.py,sha256=E_AyPPCIx_Eq-UT74QS3LKFXIwdwekRjJJGTo1CRMRY,548577
30
30
  topologicpy/Vector.py,sha256=pEC8YY3TeHGfGdeNgvdHjgMDwxGabp5aWjwYC1HSvMk,42236
31
31
  topologicpy/Vertex.py,sha256=26TrlX9OCZUN-lMlZG3g4RHTWBqw69NW4AOEgRz_YMo,91269
32
- topologicpy/Wire.py,sha256=au0ZkuuZgVzHYE5E1fRwflRT3win0yTivHKOhonAzUk,234116
32
+ topologicpy/Wire.py,sha256=Rhqw0CGEWIMVL1ICQqkCp9G-VnhhHLhEiDDR00fAn_s,248919
33
33
  topologicpy/__init__.py,sha256=RMftibjgAnHB1vdL-muo71RwMS4972JCxHuRHOlU428,928
34
- topologicpy/version.py,sha256=qAXCd2Ric5BF00XcziW6Y7EfWidI5Yii7_q1vhA8if4,23
35
- topologicpy-0.8.88.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
36
- topologicpy-0.8.88.dist-info/METADATA,sha256=TmanTsL4EPrwo0Fd1oWarCyWzj91C3AWRbJE0LMxbag,10535
37
- topologicpy-0.8.88.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- topologicpy-0.8.88.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
39
- topologicpy-0.8.88.dist-info/RECORD,,
34
+ topologicpy/version.py,sha256=Vg4EZKvQvJEbHjPwL_FRDJq1bK9YEoQB3n1ijcuv9Dk,23
35
+ topologicpy-0.8.90.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
36
+ topologicpy-0.8.90.dist-info/METADATA,sha256=Zk31JGzXHHQe7HU4kLuQ6SvCj6EnOW3PolmmmV1EepE,10535
37
+ topologicpy-0.8.90.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ topologicpy-0.8.90.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
39
+ topologicpy-0.8.90.dist-info/RECORD,,