topologicpy 0.7.69__py3-none-any.whl → 0.7.70__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/Wire.py CHANGED
@@ -1076,6 +1076,223 @@ class Wire():
1076
1076
  new_wire = Topology.SelfMerge(Topology.ByGeometry(vertices=g_vertices, edges=g_edges, faces=[]))
1077
1077
  return new_wire
1078
1078
 
1079
+
1080
+
1081
+ @staticmethod
1082
+ def ConcaveHull(topology, k: int = 3, mantissa: int = 6, tolerance: float = 0.0001):
1083
+ """
1084
+ Returns a wire representing the 2D convex hull of the input topology. The vertices of the topology are assumed to be coplanar.
1085
+ Code based on Moreira, A and Santos, M Y, "CONCAVE HULL: A K-NEAREST NEIGHBOURS APPROACH FOR THE COMPUTATION OF THE REGION OCCUPIED BY A SET OF POINTS"
1086
+ GRAPP 2007 - International Conference on Computer Graphics Theory and Applications.
1087
+
1088
+ Parameters
1089
+ ----------
1090
+ topology : topologic_core.Topology
1091
+ The input topology.
1092
+ k : int, optional
1093
+ The number of nearest neighbors to consider for each point when building the hull.
1094
+ Must be at least 3 for the algorithm to function correctly. Increasing `k` will produce a smoother,
1095
+ less concave hull, while decreasing `k` may yield a more detailed, concave shape. The default is 3.
1096
+ mantissa : int , optional
1097
+ The desired length of the mantissa. The default is 6.
1098
+ tolerance : float , optional
1099
+ The desired tolerance. The default is 0.0001.
1100
+
1101
+ Returns
1102
+ -------
1103
+ topologic_core.Wire
1104
+ The convex hull of the input topology.
1105
+ """
1106
+
1107
+ from topologicpy.Vertex import Vertex
1108
+ from topologicpy.Face import Face
1109
+ from topologicpy.Topology import Topology
1110
+ from math import atan2, sqrt, pi
1111
+ from random import sample
1112
+
1113
+ # Helper function to clean the list by removing duplicate points
1114
+ def clean_list(points_list):
1115
+ return list(set(points_list))
1116
+
1117
+ # Helper function to find the point with the minimum Y-coordinate
1118
+ def find_min_y_point(points):
1119
+ return min(points, key=lambda p: [p[1], p[0]])
1120
+
1121
+ # Helper function to find the k-nearest neighbors to a given point
1122
+ def nearest_points(points, reference_point, k):
1123
+ # Sort points by distance from the reference point and select the first k points
1124
+ sorted_points = sorted(points, key=lambda p: sqrt((p[0] - reference_point[0]) ** 2 + (p[1] - reference_point[1]) ** 2))
1125
+ return sorted_points[:k]
1126
+
1127
+ # Helper function to sort points by the angle relative to the previous direction
1128
+ def sort_by_angle(points, current_point, prev_angle):
1129
+ def angle_to(p):
1130
+ angle = atan2(p[1] - current_point[1], p[0] - current_point[0])
1131
+ angle_diff = (angle - prev_angle + 2 * pi) % (2 * pi)
1132
+ return angle_diff
1133
+ return sorted(points, key=angle_to)
1134
+
1135
+ # Helper function to check if two line segments intersect
1136
+ def intersects_q(line1, line2):
1137
+ def orientation(p, q, r):
1138
+ val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
1139
+ if val == 0: return 0
1140
+ return 1 if val > 0 else 2
1141
+
1142
+ p1, q1 = line1
1143
+ p2, q2 = line2
1144
+ o1 = orientation(p1, q1, p2)
1145
+ o2 = orientation(p1, q1, q2)
1146
+ o3 = orientation(p2, q2, p1)
1147
+ o4 = orientation(p2, q2, q1)
1148
+
1149
+ if o1 != o2 and o3 != o4:
1150
+ return True
1151
+ if o1 == 0 and on_segment(p1, p2, q1): return True
1152
+ if o2 == 0 and on_segment(p1, q2, q1): return True
1153
+ if o3 == 0 and on_segment(p2, p1, q2): return True
1154
+ if o4 == 0 and on_segment(p2, q1, q2): return True
1155
+ return False
1156
+
1157
+ # Helper function to check if point q lies on segment pr
1158
+ def on_segment(p, q, r):
1159
+ return (q[0] <= max(p[0], r[0]) and q[0] >= min(p[0], r[0]) and
1160
+ q[1] <= max(p[1], r[1]) and q[1] >= min(p[1], r[1]))
1161
+
1162
+ # Helper function to calculate the angle between two points
1163
+ def angle(p1, p2):
1164
+ return atan2(p2[1] - p1[1], p2[0] - p1[0])
1165
+
1166
+ # Helper function to determine if a point is inside a polygon (Ray Casting method)
1167
+ def point_in_polygon_q(point, polygon):
1168
+ x, y = point
1169
+ inside = False
1170
+ n = len(polygon)
1171
+ p1x, p1y = polygon[0]
1172
+ for i in range(1, n + 1):
1173
+ p2x, p2y = polygon[i % n]
1174
+ if min(p1y, p2y) < y <= max(p1y, p2y) and x <= max(p1x, p2x):
1175
+ if p1y != p2y:
1176
+ xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
1177
+ if p1x == p2x or x <= xinters:
1178
+ inside = not inside
1179
+ p1x, p1y = p2x, p2y
1180
+ return inside
1181
+
1182
+ def concave_hull(points_list, k: int = 3):
1183
+ # Ensure k >= 3
1184
+ kk = max(k, 3)
1185
+
1186
+ # Remove duplicate points
1187
+ dataset = clean_list(points_list)
1188
+
1189
+ # If there are fewer than 3 unique points, no polygon can be formed
1190
+ if len(dataset) < 3:
1191
+ return None
1192
+ elif len(dataset) == 3:
1193
+ return dataset # If exactly 3 points, they form the polygon
1194
+
1195
+ # Ensure we have enough neighbors
1196
+ kk = min(kk, len(dataset) - 1)
1197
+
1198
+ # Find starting point (minimum Y value) and initialize hull
1199
+ first_point = find_min_y_point(dataset)
1200
+ hull = [first_point]
1201
+ current_point = first_point
1202
+ dataset.remove(first_point)
1203
+ prev_angle = 0
1204
+ step = 2
1205
+
1206
+ # Original code logic, with an update to calculate prev_angle
1207
+ while (current_point != first_point or step == 2) and len(dataset) > 0:
1208
+ # After 4 steps, re-add the starting point to check for closure
1209
+ if step == 5:
1210
+ dataset.append(first_point)
1211
+
1212
+ # Find the k-nearest points
1213
+ k_nearest_points = nearest_points(dataset, current_point, kk)
1214
+
1215
+ # Sort candidates based on angle
1216
+ c_points = sort_by_angle(k_nearest_points, current_point, prev_angle)
1217
+
1218
+ intersection_found = True
1219
+ i = 0
1220
+
1221
+ # Select the first candidate that does not intersect any polygon edges
1222
+ while intersection_found and i < len(c_points):
1223
+ candidate_point = c_points[i]
1224
+ i += 1
1225
+
1226
+ if candidate_point == first_point:
1227
+ last_point_check = 1
1228
+ else:
1229
+ last_point_check = 0
1230
+
1231
+ # Check for intersections with the existing edges
1232
+ j = 2
1233
+ intersection_found = False
1234
+ while not intersection_found and j < len(hull) - last_point_check:
1235
+ # Using hull[-1] and hull[-2] for last and second-to-last points
1236
+ intersection_found = intersects_q(
1237
+ (hull[-1], candidate_point),
1238
+ (hull[-1 - j], hull[-j])
1239
+ )
1240
+ j += 1
1241
+
1242
+ # If all candidates intersect, retry with a higher number of neighbors
1243
+ if intersection_found:
1244
+ return concave_hull(points_list, kk + 1)
1245
+
1246
+ # Update the hull with the selected candidate point
1247
+ current_point = candidate_point
1248
+ hull.append(current_point)
1249
+
1250
+ # Calculate the angle between the last two points in the hull to set `prev_angle`
1251
+ if len(hull) > 1:
1252
+ prev_angle = angle(hull[-1], hull[-2])
1253
+
1254
+ dataset.remove(current_point)
1255
+ step += 1
1256
+
1257
+
1258
+ # Check if all points are inside the constructed hull
1259
+ all_inside = True
1260
+ i = len(dataset) - 1
1261
+ while all_inside and i >= 0:
1262
+ all_inside = point_in_polygon_q(dataset[i], hull)
1263
+ i -= 1
1264
+
1265
+ # If any points are outside the hull, retry with a higher number of neighbors
1266
+ if not all_inside:
1267
+ return concave_hull(points_list, kk + 1)
1268
+
1269
+ # Return the completed hull if all points are inside
1270
+ return hull
1271
+
1272
+ f = None
1273
+ # Create a sample face and flatten
1274
+ while not Topology.IsInstance(f, "Face"):
1275
+ vertices = Topology.SubTopologies(topology=topology, subTopologyType="vertex")
1276
+ v = sample(vertices, 3)
1277
+ w = Wire.ByVertices(v)
1278
+ f = Face.ByWire(w, tolerance=tolerance, silent=True)
1279
+ if not f == None:
1280
+ origin = Topology.Centroid(f)
1281
+ normal = Face.Normal(f, mantissa=mantissa)
1282
+ f = Topology.Flatten(f, origin=origin, direction=normal)
1283
+ flat_topology = Topology.Flatten(topology, origin=origin, direction=normal)
1284
+ vertices = Topology.Vertices(flat_topology)
1285
+ points = []
1286
+ for v in vertices:
1287
+ points.append((Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa)))
1288
+ hull = concave_hull(points, k=k)
1289
+ hull_vertices = []
1290
+ for p in hull:
1291
+ hull_vertices.append(Vertex.ByCoordinates(p[0], p[1], 0))
1292
+ ch = Wire.ByVertices(hull_vertices, close=True)
1293
+ ch = Topology.Unflatten(ch, origin=origin, direction=normal)
1294
+ return ch
1295
+
1079
1296
  @staticmethod
1080
1297
  def ConvexHull(topology, mantissa: int = 6, tolerance: float = 0.0001):
1081
1298
  """
@@ -1197,8 +1414,8 @@ class Wire():
1197
1414
  origin = Topology.Centroid(f)
1198
1415
  normal = Face.Normal(f, mantissa=mantissa)
1199
1416
  f = Topology.Flatten(f, origin=origin, direction=normal)
1200
- topology = Topology.Flatten(topology, origin=origin, direction=normal)
1201
- vertices = Topology.Vertices(topology)
1417
+ flat_topology = Topology.Flatten(topology, origin=origin, direction=normal)
1418
+ vertices = Topology.Vertices(flat_topology)
1202
1419
  points = []
1203
1420
  for v in vertices:
1204
1421
  points.append((Vertex.X(v, mantissa=mantissa), Vertex.Y(v, mantissa=mantissa)))
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.7.69'
1
+ __version__ = '0.7.70'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: topologicpy
3
- Version: 0.7.69
3
+ Version: 0.7.70
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
@@ -26,11 +26,11 @@ topologicpy/Sun.py,sha256=42tDWMYpwRG7Z2Qjtp94eRgBuqySq7k8TgNUZDK7QxQ,36837
26
26
  topologicpy/Topology.py,sha256=9DPTumt9kr9Su5b3RE8l_gLQSRalYHqf79tVHiy_aQM,400055
27
27
  topologicpy/Vector.py,sha256=A1g83zDHep58iVPY8WQ8iHNrSOfGWFEzvVeDuMnjDNY,33078
28
28
  topologicpy/Vertex.py,sha256=ZS6xK89JKokBKc0W8frdRhhuzR8c-dI1TTLt7pTf1iA,71032
29
- topologicpy/Wire.py,sha256=rThgAyePNkNR6LlX0lgEJZFWo7EEads1V9JdGDlZg7s,172596
29
+ topologicpy/Wire.py,sha256=LMJWcrRWtK0spuyODYANy6442lF7g-fi0KNVqcbYtJo,182071
30
30
  topologicpy/__init__.py,sha256=vlPCanUbxe5NifC4pHcnhSzkmmYcs_UrZrTlVMsxcFs,928
31
- topologicpy/version.py,sha256=UErkCOdXUeiXypvE6Y_mIRwXPLVpNPZlCmzkyCOtIFw,23
32
- topologicpy-0.7.69.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
- topologicpy-0.7.69.dist-info/METADATA,sha256=vdn5AAP-L9awRhLdq92MaSotd8o9utf6DvSQawwj_-M,10493
34
- topologicpy-0.7.69.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
35
- topologicpy-0.7.69.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
- topologicpy-0.7.69.dist-info/RECORD,,
31
+ topologicpy/version.py,sha256=B5ePMOWR_K7KFKMxVTSXxzaIt_LfFfLJBoG_ivXNV4A,23
32
+ topologicpy-0.7.70.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
+ topologicpy-0.7.70.dist-info/METADATA,sha256=sLG6R02M67ZtwLMcLs5dr-LiogkrAfuSoM4GUJa8vKw,10493
34
+ topologicpy-0.7.70.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
35
+ topologicpy-0.7.70.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
+ topologicpy-0.7.70.dist-info/RECORD,,