trenchfoot 0.3.1__py3-none-any.whl → 0.4.1__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.

Potentially problematic release.


This version of trenchfoot might be problematic. Click here for more details.

@@ -2,276 +2,211 @@
2
2
  "sdf_metadata": {
3
3
  "version": "2.0",
4
4
  "normal_convention": "into_void",
5
- "geometry_type": "open_trench",
5
+ "geometry_type": "closed_well",
6
6
  "trench_opening": {
7
7
  "type": "polygon",
8
8
  "vertices_xy": [
9
9
  [
10
- 0.5048152733278032,
11
- -0.0980171403295607
10
+ 2.5048385723763116,
11
+ -2.7755575615628914e-15
12
12
  ],
13
13
  [
14
- 0.5142375848726368,
15
- 0.0023508057697296803
14
+ 2.456708801572928,
15
+ 0.4886697636833139
16
16
  ],
17
17
  [
18
- 0.5038980344185748,
19
- 0.10262841172163711
18
+ 2.3141690892632654,
19
+ 0.9585602223974381
20
20
  ],
21
21
  [
22
- 0.47419396509108047,
23
- 0.198962065365758
22
+ 2.082697156654193,
23
+ 1.3916137493315945
24
24
  ],
25
25
  [
26
- 0.4262668876161758,
27
- 0.2876497184170843
26
+ 1.7711883403049211,
27
+ 1.77118834030492
28
28
  ],
29
29
  [
30
- 0.3619586127034061,
31
- 0.36528315410546264
30
+ 1.3916137493315943,
31
+ 2.082697156654193
32
32
  ],
33
33
  [
34
- 0.28374047129317204,
35
- 0.4288789630347212
34
+ 0.9585602223974379,
35
+ 2.3141690892632654
36
36
  ],
37
37
  [
38
- 0.1946183426946318,
39
- 0.47599319393264883
38
+ 0.488669763683314,
39
+ 2.456708801572928
40
40
  ],
41
41
  [
42
- 0.0980171403295608,
43
- 0.5048152733278032
42
+ 9.71445146547012e-17,
43
+ 2.504838572376311
44
44
  ],
45
45
  [
46
- -0.0023508057697296803,
47
- 0.5142375848726368
46
+ -0.48866976368331355,
47
+ 2.456708801572928
48
48
  ],
49
49
  [
50
- -0.10262841172163778,
51
- 0.5038980344185745
50
+ -0.9585602223974382,
51
+ 2.314169089263265
52
52
  ],
53
53
  [
54
- -0.19896206536575745,
55
- 0.47419396509108125
54
+ -1.3916137493315945,
55
+ 2.082697156654193
56
56
  ],
57
57
  [
58
- -0.2876497184170844,
59
- 0.4262668876161758
58
+ -1.7711883403049202,
59
+ 1.7711883403049207
60
60
  ],
61
61
  [
62
- -0.36528315410546275,
63
- 0.361958612703406
62
+ -2.0826971566541927,
63
+ 1.3916137493315948
64
64
  ],
65
65
  [
66
- -0.4288789630347214,
67
- 0.2837404712931719
66
+ -2.314169089263265,
67
+ 0.9585602223974394
68
68
  ],
69
69
  [
70
- -0.47599319393264883,
71
- 0.1946183426946323
70
+ -2.456708801572928,
71
+ 0.48866976368331483
72
72
  ],
73
73
  [
74
- -0.5048152733278032,
75
- 0.09801714032956078
74
+ -2.504838572376311,
75
+ 1.8041124150158794e-16
76
76
  ],
77
77
  [
78
- -0.5142375848726367,
79
- -0.0023508057697302354
78
+ -2.4567088015729275,
79
+ -0.48866976368331533
80
80
  ],
81
81
  [
82
- -0.503898034418575,
83
- -0.10262841172163728
82
+ -2.3141690892632654,
83
+ -0.958560222397438
84
84
  ],
85
85
  [
86
- -0.47419396509108125,
87
- -0.19896206536575745
86
+ -2.0826971566541936,
87
+ -1.3916137493315939
88
88
  ],
89
89
  [
90
- -0.42626688761617615,
91
- -0.287649718417084
90
+ -1.7711883403049204,
91
+ -1.7711883403049207
92
92
  ],
93
93
  [
94
- -0.36195861270340507,
95
- -0.36528315410546297
94
+ -1.3916137493315952,
95
+ -2.0826971566541927
96
96
  ],
97
97
  [
98
- -0.2837404712931731,
99
- -0.42887896303472095
98
+ -0.9585602223974392,
99
+ -2.314169089263265
100
100
  ],
101
101
  [
102
- -0.19461834269463169,
103
- -0.4759931939326486
102
+ -0.48866976368331394,
103
+ -2.456708801572928
104
104
  ],
105
105
  [
106
- -0.09801714032956087,
107
- -0.5048152733278032
106
+ -1.0408340855860843e-15,
107
+ -2.504838572376311
108
108
  ],
109
109
  [
110
- 0.0023508057697300133,
111
- -0.5142375848726368
110
+ 0.4886697636833134,
111
+ -2.456708801572928
112
112
  ],
113
113
  [
114
- 0.1026284117216375,
115
- -0.5038980344185748
114
+ 0.9585602223974385,
115
+ -2.314169089263265
116
116
  ],
117
117
  [
118
- 0.19896206536575722,
119
- -0.47419396509108147
118
+ 1.3916137493315957,
119
+ -2.0826971566541923
120
120
  ],
121
121
  [
122
- 0.2876497184170842,
123
- -0.4262668876161758
122
+ 1.7711883403049202,
123
+ -1.7711883403049207
124
124
  ],
125
125
  [
126
- 0.36528315410546297,
127
- -0.36195861270340507
126
+ 2.0826971566541927,
127
+ -1.391613749331595
128
128
  ],
129
129
  [
130
- 0.42887896303472095,
131
- -0.28374047129317304
130
+ 2.314169089263265,
131
+ -0.9585602223974392
132
132
  ],
133
133
  [
134
- 0.4759931939326485,
135
- -0.19461834269463185
136
- ],
137
- [
138
- 0.5048152733278031,
139
- 0.09801714032956123
140
- ],
141
- [
142
- 2.495184726672197,
143
- -0.09801714032956123
144
- ],
145
- [
146
- 2.4663626472770424,
147
- -0.3906526233537543
148
- ],
149
- [
150
- 2.3427596344991386,
151
- -0.864309825802098
152
- ],
153
- [
154
- 2.1291256828021727,
155
- -1.3047520863554014
156
- ],
157
- [
158
- 1.8336706251425579,
159
- -1.6950534559434671
160
- ],
161
- [
162
- 1.4677486336930485,
163
- -2.020214871816555
164
- ],
165
- [
166
- 1.0454218853736323,
167
- -2.267740563115285
168
- ],
169
- [
170
- 0.5829201602786549,
171
- -2.4281182563370542
172
- ],
173
- [
174
- 0.09801714032956031,
175
- -2.4951847266721967
176
- ],
177
- [
178
- -0.39065262335375434,
179
- -2.4663626472770424
180
- ],
181
- [
182
- -0.864309825802098,
183
- -2.3427596344991386
184
- ],
185
- [
186
- -1.3047520863554014,
187
- -2.1291256828021727
188
- ],
189
- [
190
- -1.6950534559434667,
191
- -1.8336706251425583
192
- ],
193
- [
194
- -2.020214871816555,
195
- -1.4677486336930485
196
- ],
197
- [
198
- -2.2677405631152854,
199
- -1.045421885373632
200
- ],
201
- [
202
- -2.4281182563370547,
203
- -0.5829201602786548
204
- ],
205
- [
206
- -2.4951847266721967,
207
- -0.09801714032956042
208
- ],
209
- [
210
- -2.4663626472770424,
211
- 0.3906526233537535
212
- ],
213
- [
214
- -2.3427596344991386,
215
- 0.8643098258020978
216
- ],
217
- [
218
- -2.129125682802173,
219
- 1.3047520863554005
220
- ],
221
- [
222
- -1.8336706251425579,
223
- 1.6950534559434671
224
- ],
225
- [
226
- -1.4677486336930485,
227
- 2.020214871816555
228
- ],
229
- [
230
- -1.0454218853736315,
231
- 2.2677405631152854
232
- ],
233
- [
234
- -0.5829201602786549,
235
- 2.4281182563370542
236
- ],
237
- [
238
- -0.0980171403295606,
239
- 2.4951847266721967
240
- ],
241
- [
242
- 0.3906526233537532,
243
- 2.4663626472770424
244
- ],
245
- [
246
- 0.8643098258020975,
247
- 2.3427596344991386
248
- ],
249
- [
250
- 1.3047520863554007,
251
- 2.129125682802173
252
- ],
253
- [
254
- 1.6950534559434671,
255
- 1.833670625142558
256
- ],
257
- [
258
- 2.020214871816555,
259
- 1.4677486336930485
260
- ],
261
- [
262
- 2.267740563115285,
263
- 1.0454218853736321
264
- ],
265
- [
266
- 2.4281182563370542,
267
- 0.5829201602786551
268
- ],
269
- [
270
- 2.4951847266721967,
271
- 0.0980171403295607
134
+ 2.456708801572928,
135
+ -0.4886697636833141
272
136
  ]
273
137
  ],
274
138
  "z_level": 0.0
139
+ },
140
+ "surface_groups": {
141
+ "trench_bottom": {
142
+ "normal_direction": "up",
143
+ "surface_type": "floor"
144
+ },
145
+ "trench_walls": {
146
+ "normal_direction": "inward",
147
+ "surface_type": "wall"
148
+ },
149
+ "ground_surface": {
150
+ "normal_direction": "up",
151
+ "surface_type": "ground"
152
+ },
153
+ "pipe0_pipe_side": {
154
+ "normal_direction": "outward",
155
+ "surface_type": "embedded_object"
156
+ },
157
+ "pipe0_pipe_cap_neg": {
158
+ "normal_direction": "outward",
159
+ "surface_type": "embedded_object"
160
+ },
161
+ "pipe0_pipe_cap_pos": {
162
+ "normal_direction": "outward",
163
+ "surface_type": "embedded_object"
164
+ },
165
+ "pipe1_pipe_side": {
166
+ "normal_direction": "outward",
167
+ "surface_type": "embedded_object"
168
+ },
169
+ "pipe1_pipe_cap_neg": {
170
+ "normal_direction": "outward",
171
+ "surface_type": "embedded_object"
172
+ },
173
+ "pipe1_pipe_cap_pos": {
174
+ "normal_direction": "outward",
175
+ "surface_type": "embedded_object"
176
+ },
177
+ "pipe2_pipe_side": {
178
+ "normal_direction": "outward",
179
+ "surface_type": "embedded_object"
180
+ },
181
+ "pipe2_pipe_cap_neg": {
182
+ "normal_direction": "outward",
183
+ "surface_type": "embedded_object"
184
+ },
185
+ "pipe2_pipe_cap_pos": {
186
+ "normal_direction": "outward",
187
+ "surface_type": "embedded_object"
188
+ },
189
+ "pipe3_pipe_side": {
190
+ "normal_direction": "outward",
191
+ "surface_type": "embedded_object"
192
+ },
193
+ "pipe3_pipe_cap_neg": {
194
+ "normal_direction": "outward",
195
+ "surface_type": "embedded_object"
196
+ },
197
+ "pipe3_pipe_cap_pos": {
198
+ "normal_direction": "outward",
199
+ "surface_type": "embedded_object"
200
+ },
201
+ "sphere0": {
202
+ "normal_direction": "outward",
203
+ "surface_type": "embedded_object"
204
+ }
205
+ },
206
+ "embedded_objects": {
207
+ "pipes": 4,
208
+ "boxes": 0,
209
+ "spheres": 1
275
210
  }
276
211
  }
277
212
  }
@@ -34,7 +34,7 @@ except Exception:
34
34
  Poly3DCollection = None
35
35
 
36
36
  # Groups kept for internal metrics but excluded from OBJ export and previews
37
- _INTERNAL_GROUPS = frozenset({"trench_cap_for_volume"})
37
+ _INTERNAL_GROUPS = frozenset({"trench_cap_for_volume", "inner_column_lid"})
38
38
 
39
39
  # ---------------- Geometry helpers ----------------
40
40
 
@@ -123,6 +123,9 @@ def _offset_closed_polyline(path: List[Tuple[float,float]], offset: float) -> Li
123
123
  this returns a single continuous closed ring for paths where first ≈ last point.
124
124
  """
125
125
  P = np.array(path, float)
126
+ # Remove duplicate closing point if present (first ≈ last)
127
+ if len(P) > 1 and np.linalg.norm(P[0] - P[-1]) < 0.01:
128
+ P = P[:-1]
126
129
  n = len(P)
127
130
  if n < 3:
128
131
  raise ValueError("Closed polyline needs at least 3 points")
@@ -195,6 +198,74 @@ def _ear_clipping_triangulation(poly_xy: np.ndarray) -> np.ndarray:
195
198
  tris.append([V[0],V[1],V[2]])
196
199
  return np.array(tris,int)
197
200
 
201
+
202
+ def _extract_boundary_polygon(V: np.ndarray, F: np.ndarray) -> Optional[np.ndarray]:
203
+ """Extract ordered boundary polygon from a triangulated mesh.
204
+
205
+ Finds boundary edges (edges appearing in only one face) and chains
206
+ them together to form an ordered polygon. This correctly handles
207
+ non-convex shapes like L-shaped or U-shaped boundaries.
208
+
209
+ Parameters
210
+ ----------
211
+ V : np.ndarray
212
+ Vertices (n, 3) or (n, 2)
213
+ F : np.ndarray
214
+ Faces (m, 3) - triangle indices
215
+
216
+ Returns
217
+ -------
218
+ np.ndarray or None
219
+ Ordered boundary vertices XY coordinates (k, 2), or None if
220
+ no boundary edges found (closed mesh).
221
+ """
222
+ if F.size == 0:
223
+ return None
224
+
225
+ # Count edge occurrences - boundary edges appear exactly once
226
+ edge_count: Dict[Tuple[int, int], int] = {}
227
+ for face in F:
228
+ for i in range(3):
229
+ v0, v1 = int(face[i]), int(face[(i + 1) % 3])
230
+ edge = (min(v0, v1), max(v0, v1))
231
+ edge_count[edge] = edge_count.get(edge, 0) + 1
232
+
233
+ # Boundary edges appear exactly once
234
+ boundary_edges = [edge for edge, count in edge_count.items() if count == 1]
235
+
236
+ if not boundary_edges:
237
+ return None # No boundary (closed mesh)
238
+
239
+ # Build adjacency from boundary edges
240
+ adj: Dict[int, List[int]] = {}
241
+ for v0, v1 in boundary_edges:
242
+ adj.setdefault(v0, []).append(v1)
243
+ adj.setdefault(v1, []).append(v0)
244
+
245
+ # Chain the boundary edges starting from any vertex
246
+ start = boundary_edges[0][0]
247
+ polygon = [start]
248
+ prev = None
249
+ curr = start
250
+
251
+ max_iter = len(boundary_edges) + 1
252
+ for _ in range(max_iter):
253
+ neighbors = adj.get(curr, [])
254
+ # Pick the neighbor that isn't the previous vertex
255
+ next_candidates = [n for n in neighbors if n != prev]
256
+ if not next_candidates:
257
+ break
258
+ next_v = next_candidates[0]
259
+ if next_v == start:
260
+ break # Completed the loop
261
+ polygon.append(next_v)
262
+ prev = curr
263
+ curr = next_v
264
+
265
+ # Extract XY coordinates
266
+ return V[polygon, :2]
267
+
268
+
198
269
  # ---------------- Mesh IO & metrics ----------------
199
270
 
200
271
  def write_obj_with_groups(path: str, groups: Dict[str, Tuple[np.ndarray, np.ndarray]]):
@@ -405,6 +476,7 @@ class GroundSpec:
405
476
  z0: float = 0.0
406
477
  slope: Tuple[float,float] = (0.0, 0.0) # (dz/dx, dz/dy)
407
478
  size_margin: float = 3.0
479
+ fill_interior: bool = False # For closed paths: fill the interior with ground surface
408
480
 
409
481
  @dataclass
410
482
  class SceneSpec:
@@ -447,19 +519,15 @@ class SurfaceMeshResult:
447
519
  if "trench_cap_for_volume" in self.groups:
448
520
  V_cap, F_cap = self.groups["trench_cap_for_volume"]
449
521
  if V_cap.size > 0:
450
- # Get unique vertices at z ground level (the top cap boundary)
451
- # These form the trench opening polygon
452
- z_level = float(np.median(V_cap[:, 2]))
453
- xy_coords = V_cap[:, :2]
454
- # Use convex hull to get ordered boundary vertices
455
- try:
456
- from scipy.spatial import ConvexHull
457
- hull = ConvexHull(xy_coords)
458
- boundary_indices = hull.vertices
459
- trench_opening_vertices = xy_coords[boundary_indices].tolist()
460
- except ImportError:
461
- # Fallback: just use unique xy coords (unordered)
462
- trench_opening_vertices = xy_coords.tolist()
522
+ # Extract boundary polygon by finding boundary edges (edges in only one face)
523
+ # and chaining them together. This correctly handles non-convex shapes
524
+ # like L-shaped or U-shaped trenches.
525
+ boundary_xy = _extract_boundary_polygon(V_cap, F_cap)
526
+ if boundary_xy is not None:
527
+ trench_opening_vertices = boundary_xy.tolist()
528
+ else:
529
+ # Fallback: just use all vertices (unordered)
530
+ trench_opening_vertices = V_cap[:, :2].tolist()
463
531
 
464
532
  # Determine geometry type
465
533
  is_closed = _is_path_closed(self.spec.path_xy)
@@ -485,7 +553,7 @@ class SurfaceMeshResult:
485
553
 
486
554
  return {
487
555
  "sdf_metadata": {
488
- "version": "1.0",
556
+ "version": "2.0",
489
557
  "normal_convention": "into_void",
490
558
  "geometry_type": geometry_type,
491
559
  "trench_opening": {
@@ -735,6 +803,16 @@ def make_trench_from_path_sloped(path_xy: List[Tuple[float,float]], width_top: f
735
803
  V_walls = np.array(walls_V, float)
736
804
  F_walls = np.array(walls_F, int)
737
805
 
806
+ # Inner column lid: cap the top of the inner column at ground level
807
+ # Reverse inner_top to get CCW winding for upward-facing normals
808
+ inner_top_ccw = inner_top[::-1].copy()
809
+ z_inner_top_ccw = z_inner_top[::-1].copy()
810
+ lid_xy, lid_faces = _triangulate_polygon_fan(inner_top_ccw)
811
+ # Assign z-values: polygon vertices use z_inner_top_ccw, centroid uses average
812
+ z_lid = np.concatenate([z_inner_top_ccw, [np.mean(z_inner_top_ccw)]])
813
+ V_lid = np.column_stack([lid_xy, z_lid])
814
+ F_lid = _ensure_upward_normals(V_lid, lid_faces)
815
+
738
816
  # For closed path, poly_top is the outer ring (used for ground plane hole)
739
817
  poly_top = outer_top
740
818
  poly_bot = outer_bot
@@ -796,6 +874,10 @@ def make_trench_from_path_sloped(path_xy: List[Tuple[float,float]], width_top: f
796
874
  "trench_walls": (V_walls, F_walls)
797
875
  }
798
876
 
877
+ # Add inner column lid for closed paths
878
+ if is_closed:
879
+ groups["inner_column_lid"] = (V_lid, F_lid)
880
+
799
881
  return groups, poly_top, poly_bot, extra
800
882
 
801
883
  def _ensure_upward_normals(V: np.ndarray, F: np.ndarray) -> np.ndarray:
@@ -830,6 +912,28 @@ def _ensure_upward_normals(V: np.ndarray, F: np.ndarray) -> np.ndarray:
830
912
  return F_out
831
913
 
832
914
 
915
+ def _triangulate_polygon_fan(polygon: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
916
+ """Triangulate a simple polygon using fan triangulation from centroid.
917
+
918
+ Works well for convex or nearly-convex polygons. Returns (vertices, faces)
919
+ where vertices includes the original polygon points plus the centroid.
920
+ """
921
+ n = len(polygon)
922
+ centroid = polygon.mean(axis=0)
923
+
924
+ # Vertices: polygon points first, then centroid at the end
925
+ verts = np.vstack([polygon, centroid.reshape(1, -1)])
926
+ centroid_idx = n
927
+
928
+ # Fan triangles from centroid to each edge
929
+ tris = []
930
+ for i in range(n):
931
+ j = (i + 1) % n
932
+ tris.append([centroid_idx, i, j])
933
+
934
+ return verts, np.array(tris, dtype=int)
935
+
936
+
833
937
  def _triangulate_annulus(outer: np.ndarray, inner: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
834
938
  """Triangulate the annular region between outer and inner polygons.
835
939
 
@@ -919,9 +1023,8 @@ def make_ground_surface_plane(path_xy: List[Tuple[float,float]], width_top: floa
919
1023
  is_closed = _is_path_closed(path_xy)
920
1024
 
921
1025
  if is_closed:
922
- # For closed paths (like circular wells), the ground is an annulus
923
- # from the outer ground boundary to the outer edge of the trench opening.
924
- # The center (inside the trench) is left completely open.
1026
+ # For closed paths, ground is an annulus from outer boundary to trench edge.
1027
+ # Optionally, if fill_interior is set, also fill the interior island.
925
1028
 
926
1029
  # Trench outer boundary (edge of trench opening)
927
1030
  trench_outer = np.array(_offset_closed_polyline(path_xy, half_top), float)
@@ -933,14 +1036,23 @@ def make_ground_surface_plane(path_xy: List[Tuple[float,float]], width_top: floa
933
1036
  trench_outer = _ensure_ccw(trench_outer)
934
1037
  ground_outer = _ensure_ccw(ground_outer)
935
1038
 
936
- # Ground annulus: from ground_outer to trench_outer
937
- combined_xy, tris = _triangulate_annulus(ground_outer, trench_outer)
938
- Vg = np.array([[x, y, gfun(x, y)] for (x, y) in combined_xy], float)
1039
+ # Outer ground annulus: from ground_outer to trench_outer
1040
+ outer_xy, outer_tris = _triangulate_annulus(ground_outer, trench_outer)
1041
+ Vg_outer = np.array([[x, y, gfun(x, y)] for (x, y) in outer_xy], float)
1042
+ outer_tris = _ensure_upward_normals(Vg_outer, outer_tris)
939
1043
 
940
- # Ground normals should point UP (+z) into the air
941
- tris = _ensure_upward_normals(Vg, tris)
1044
+ result = {"ground_surface": (Vg_outer, outer_tris)}
942
1045
 
943
- return {"ground_surface": (Vg, tris)}
1046
+ # Optionally fill the interior island (for loop trenches, not wells/pits)
1047
+ if getattr(ground, 'fill_interior', False):
1048
+ trench_inner = np.array(_offset_closed_polyline(path_xy, -half_top), float)
1049
+ trench_inner = _ensure_ccw(trench_inner)
1050
+ inner_xy, inner_tris = _triangulate_polygon_fan(trench_inner)
1051
+ Vg_inner = np.array([[x, y, gfun(x, y)] for (x, y) in inner_xy], float)
1052
+ inner_tris = _ensure_upward_normals(Vg_inner, inner_tris)
1053
+ result["ground_island"] = (Vg_inner, inner_tris)
1054
+
1055
+ return result
944
1056
  else:
945
1057
  # Open paths: ground forms annulus with extensions past trench endpoints.
946
1058
  # The outer ring (ground boundary) is extended, but the inner ring (trench
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trenchfoot
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: Synthetic trench scenario generator bundle (surfaces + volumetrics).
5
5
  Author: Liam Moore
6
6
  License-File: LICENSE