trenchfoot 0.2.7__tar.gz → 0.3.0__tar.gz
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.
- trenchfoot-0.3.0/.pypi_token.env +1 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/CHANGELOG.md +19 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/PKG-INFO +1 -1
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/trench_scene_generator_v3.py +125 -4
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/pyproject.toml +1 -1
- trenchfoot-0.3.0/tests/test_sdf_readiness.py +385 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/uv.lock +1 -1
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/.env +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/.github/workflows/ci.yml +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/.github_token.env +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/CLAUDE.md +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/LICENSE +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/README.md +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/STATUS.md +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/docs/scenario_gallery.md +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/__init__.py +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/Dockerfile +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/README.md +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/__init__.py +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/generate_scenarios.py +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/gmsh_sloped_trench_mesher.py +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/plot_mesh.py +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/render_colors.py +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/meshes/trench_scene_culled.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/metrics.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/point_clouds/culled/resolution0p050.pth +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/point_clouds/full/resolution0p050.pth +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview_oblique.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview_side.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview_top.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/scene.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/trench_scene.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/volumetric/trench_volume.msh +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/meshes/trench_scene_culled.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/metrics.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/point_clouds/culled/resolution0p050.pth +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/point_clouds/full/resolution0p050.pth +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview_oblique.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview_side.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/preview_top.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/scene.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/trench_scene.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S02_straight_slope_pipe/volumetric/trench_volume.msh +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/meshes/trench_scene_culled.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/metrics.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/point_clouds/culled/resolution0p050.pth +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/point_clouds/full/resolution0p050.pth +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_oblique.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_side.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/preview_top.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/scene.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/trench_scene.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S03_L_slope_two_pipes_box/volumetric/trench_volume.msh +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/meshes/trench_scene_culled.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/metrics.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview_oblique.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview_side.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/preview_top.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/scene.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/trench_scene.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S04_U_slope_multi_noise/volumetric/trench_volume.msh +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/metrics.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/preview_oblique.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/preview_side.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/preview_top.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/scene.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/trench_scene.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/volumetric/trench_volume.msh +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/metrics.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/preview_oblique.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/preview_side.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/preview_top.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/scene.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/trench_scene.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/volumetric/trench_volume.msh +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/metrics.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/preview_oblique.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/preview_side.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/preview_top.png +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/scene.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/trench_scene.obj +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/volumetric/trench_volume.msh +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/SUMMARY.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scene_spec_example.json +0 -0
- {trenchfoot-0.2.7 → trenchfoot-0.3.0}/tests/test_trenchfoot_generation.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PYPI_API_TOKEN="pypi-AgEIcHlwaS5vcmcCJDU5NWZhMThiLTcwZDAtNGQ1NS04NzY5LTUzOWFjNzUyMzBhOQACKlszLCJiYTQ1Nzc2Ni01MDg0LTQ4ZmYtOGU5Yi05MjY4NjM3MTNiY2EiXQAABiCDzIFj_CDx2Hr1RoMANFCByB8X_u1VLbmPLNpe-oOFow"
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-01-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **SDF metadata export**: Each scenario now exports `sdf_metadata.json` alongside the OBJ mesh, containing:
|
|
7
|
+
- Normal convention ("into_void")
|
|
8
|
+
- Geometry type ("open_trench")
|
|
9
|
+
- Trench opening polygon with z-level
|
|
10
|
+
- Surface group annotations (floor, walls, ground)
|
|
11
|
+
- Embedded object counts
|
|
12
|
+
- **SDF readiness tests**: 21 new tests in `tests/test_sdf_readiness.py` validating:
|
|
13
|
+
- Floor normals point up (+z)
|
|
14
|
+
- Wall normals point inward toward trench centerline
|
|
15
|
+
- Ground surface normals point up (+z)
|
|
16
|
+
- Metadata consistency with mesh geometry
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Normal orientation consistency**: All floor and ground surfaces now have upward-pointing normals (+z). Previously, some faces (particularly on S07 circular well ground surface) had incorrect winding order causing downward normals, which broke SDF sign computation in downstream consumers.
|
|
20
|
+
- **`_ensure_upward_normals()` post-processing**: Added helper function that flips face winding order for any horizontal faces with downward normals, applied to all ground surface generation paths.
|
|
21
|
+
|
|
3
22
|
## [0.2.7] - 2025-12-24
|
|
4
23
|
|
|
5
24
|
### Fixed
|
|
@@ -425,6 +425,7 @@ class SurfaceMeshFiles:
|
|
|
425
425
|
obj_path: Path
|
|
426
426
|
metrics_path: Path
|
|
427
427
|
preview_paths: Tuple[Path, ...]
|
|
428
|
+
sdf_metadata_path: Optional[Path] = None
|
|
428
429
|
|
|
429
430
|
|
|
430
431
|
@dataclass
|
|
@@ -435,7 +436,73 @@ class SurfaceMeshResult:
|
|
|
435
436
|
metrics: Dict[str, Any]
|
|
436
437
|
previews: Dict[str, bytes]
|
|
437
438
|
|
|
438
|
-
def
|
|
439
|
+
def _build_sdf_metadata(self) -> Dict[str, Any]:
|
|
440
|
+
"""Build SDF metadata for downstream consumers.
|
|
441
|
+
|
|
442
|
+
This metadata enables generic mesh-to-SDF pipelines to correctly
|
|
443
|
+
interpret the mesh geometry without trenchfoot-specific heuristics.
|
|
444
|
+
"""
|
|
445
|
+
# Extract trench opening polygon from the trench cap geometry
|
|
446
|
+
trench_opening_vertices = None
|
|
447
|
+
if "trench_cap_for_volume" in self.groups:
|
|
448
|
+
V_cap, F_cap = self.groups["trench_cap_for_volume"]
|
|
449
|
+
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()
|
|
463
|
+
|
|
464
|
+
# Determine geometry type
|
|
465
|
+
is_closed = _is_path_closed(self.spec.path_xy)
|
|
466
|
+
geometry_type = "closed_well" if is_closed else "open_trench"
|
|
467
|
+
|
|
468
|
+
# Build surface group info
|
|
469
|
+
surface_groups = {}
|
|
470
|
+
for name in self.groups:
|
|
471
|
+
if name in _INTERNAL_GROUPS:
|
|
472
|
+
continue
|
|
473
|
+
if "bottom" in name:
|
|
474
|
+
surface_groups[name] = {"normal_direction": "up", "surface_type": "floor"}
|
|
475
|
+
elif "wall" in name:
|
|
476
|
+
surface_groups[name] = {"normal_direction": "inward", "surface_type": "wall"}
|
|
477
|
+
elif "ground" in name:
|
|
478
|
+
surface_groups[name] = {"normal_direction": "up", "surface_type": "ground"}
|
|
479
|
+
elif "pipe" in name:
|
|
480
|
+
surface_groups[name] = {"normal_direction": "outward", "surface_type": "embedded_object"}
|
|
481
|
+
elif "box" in name or "sphere" in name:
|
|
482
|
+
surface_groups[name] = {"normal_direction": "outward", "surface_type": "embedded_object"}
|
|
483
|
+
else:
|
|
484
|
+
surface_groups[name] = {"normal_direction": "unknown", "surface_type": "other"}
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
"sdf_metadata": {
|
|
488
|
+
"version": "1.0",
|
|
489
|
+
"normal_convention": "into_void",
|
|
490
|
+
"geometry_type": geometry_type,
|
|
491
|
+
"trench_opening": {
|
|
492
|
+
"type": "polygon",
|
|
493
|
+
"vertices_xy": trench_opening_vertices,
|
|
494
|
+
"z_level": self.spec.ground.z0 if self.spec.ground else 0.0,
|
|
495
|
+
},
|
|
496
|
+
"surface_groups": surface_groups,
|
|
497
|
+
"embedded_objects": {
|
|
498
|
+
"pipes": self.object_counts.get("pipes", 0),
|
|
499
|
+
"boxes": self.object_counts.get("boxes", 0),
|
|
500
|
+
"spheres": self.object_counts.get("spheres", 0),
|
|
501
|
+
},
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
def persist(self, out_dir: str | Path, *, include_previews: bool = False, include_sdf_metadata: bool = True) -> SurfaceMeshFiles:
|
|
439
506
|
out_path = Path(out_dir)
|
|
440
507
|
out_path.mkdir(parents=True, exist_ok=True)
|
|
441
508
|
obj_path = out_path / "trench_scene.obj"
|
|
@@ -445,13 +512,27 @@ class SurfaceMeshResult:
|
|
|
445
512
|
metrics_path = out_path / "metrics.json"
|
|
446
513
|
with metrics_path.open("w") as fh:
|
|
447
514
|
json.dump(self.metrics, fh, indent=2)
|
|
515
|
+
|
|
516
|
+
# Export SDF metadata
|
|
517
|
+
sdf_metadata_path = None
|
|
518
|
+
if include_sdf_metadata:
|
|
519
|
+
sdf_metadata = self._build_sdf_metadata()
|
|
520
|
+
sdf_metadata_path = out_path / "sdf_metadata.json"
|
|
521
|
+
with sdf_metadata_path.open("w") as fh:
|
|
522
|
+
json.dump(sdf_metadata, fh, indent=2)
|
|
523
|
+
|
|
448
524
|
preview_paths: List[Path] = []
|
|
449
525
|
if include_previews and self.previews:
|
|
450
526
|
for name, data in self.previews.items():
|
|
451
527
|
target = out_path / f"preview_{name}.png"
|
|
452
528
|
target.write_bytes(data)
|
|
453
529
|
preview_paths.append(target)
|
|
454
|
-
return SurfaceMeshFiles(
|
|
530
|
+
return SurfaceMeshFiles(
|
|
531
|
+
obj_path=obj_path,
|
|
532
|
+
metrics_path=metrics_path,
|
|
533
|
+
preview_paths=tuple(preview_paths),
|
|
534
|
+
sdf_metadata_path=sdf_metadata_path,
|
|
535
|
+
)
|
|
455
536
|
|
|
456
537
|
def _ground_fn(g: GroundSpec):
|
|
457
538
|
sx, sy = g.slope
|
|
@@ -620,7 +701,8 @@ def make_trench_from_path_sloped(path_xy: List[Tuple[float,float]], width_top: f
|
|
|
620
701
|
|
|
621
702
|
bot_verts, bot_faces = _triangulate_annulus(outer_bot, inner_bot[::-1])
|
|
622
703
|
V_bottom = np.column_stack([bot_verts, np.concatenate([z_outer_bot, z_inner_bot[::-1]])])
|
|
623
|
-
|
|
704
|
+
# Floor normals point UP (+z) into the trench void for correct SDF sign
|
|
705
|
+
F_bottom = _ensure_upward_normals(V_bottom, bot_faces)
|
|
624
706
|
|
|
625
707
|
# Outer wall: connects outer_top to outer_bot (facing outward from trench)
|
|
626
708
|
n_outer = len(outer_top)
|
|
@@ -680,7 +762,8 @@ def make_trench_from_path_sloped(path_xy: List[Tuple[float,float]], width_top: f
|
|
|
680
762
|
V_cap = np.column_stack([poly_top, z_top])
|
|
681
763
|
V_bottom = np.column_stack([poly_bot, z_bot])
|
|
682
764
|
F_cap = tris_top
|
|
683
|
-
|
|
765
|
+
# Floor normals point UP (+z) into the trench void for correct SDF sign
|
|
766
|
+
F_bottom = _ensure_upward_normals(V_bottom, tris_bot)
|
|
684
767
|
|
|
685
768
|
# Walls: connect corresponding indices
|
|
686
769
|
N = len(poly_top)
|
|
@@ -715,6 +798,38 @@ def make_trench_from_path_sloped(path_xy: List[Tuple[float,float]], width_top: f
|
|
|
715
798
|
|
|
716
799
|
return groups, poly_top, poly_bot, extra
|
|
717
800
|
|
|
801
|
+
def _ensure_upward_normals(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
|
802
|
+
"""Ensure all faces have upward-pointing normals (+z).
|
|
803
|
+
|
|
804
|
+
For horizontal surfaces like trench floors, normals should point UP
|
|
805
|
+
into the void for correct SDF computation. This function flips any
|
|
806
|
+
faces with downward-pointing normals.
|
|
807
|
+
|
|
808
|
+
Parameters
|
|
809
|
+
----------
|
|
810
|
+
V : np.ndarray
|
|
811
|
+
Vertices (n, 3)
|
|
812
|
+
F : np.ndarray
|
|
813
|
+
Faces (m, 3) - indices into V
|
|
814
|
+
|
|
815
|
+
Returns
|
|
816
|
+
-------
|
|
817
|
+
np.ndarray
|
|
818
|
+
Faces with consistent upward normals (may have winding flipped)
|
|
819
|
+
"""
|
|
820
|
+
F_out = F.copy()
|
|
821
|
+
p0 = V[F[:, 0]]
|
|
822
|
+
p1 = V[F[:, 1]]
|
|
823
|
+
p2 = V[F[:, 2]]
|
|
824
|
+
normals = np.cross(p1 - p0, p2 - p0)
|
|
825
|
+
|
|
826
|
+
# Flip faces with negative z-component normals
|
|
827
|
+
down_mask = normals[:, 2] < 0
|
|
828
|
+
F_out[down_mask] = F_out[down_mask, ::-1]
|
|
829
|
+
|
|
830
|
+
return F_out
|
|
831
|
+
|
|
832
|
+
|
|
718
833
|
def _triangulate_annulus(outer: np.ndarray, inner: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
719
834
|
"""Triangulate the annular region between outer and inner polygons.
|
|
720
835
|
|
|
@@ -822,6 +937,9 @@ def make_ground_surface_plane(path_xy: List[Tuple[float,float]], width_top: floa
|
|
|
822
937
|
combined_xy, tris = _triangulate_annulus(ground_outer, trench_outer)
|
|
823
938
|
Vg = np.array([[x, y, gfun(x, y)] for (x, y) in combined_xy], float)
|
|
824
939
|
|
|
940
|
+
# Ground normals should point UP (+z) into the air
|
|
941
|
+
tris = _ensure_upward_normals(Vg, tris)
|
|
942
|
+
|
|
825
943
|
return {"ground_surface": (Vg, tris)}
|
|
826
944
|
else:
|
|
827
945
|
# Open paths: ground forms annulus with extensions past trench endpoints.
|
|
@@ -844,6 +962,9 @@ def make_ground_surface_plane(path_xy: List[Tuple[float,float]], width_top: floa
|
|
|
844
962
|
# Apply ground elevation to get 3D vertices
|
|
845
963
|
Vg = np.array([[x, y, gfun(x, y)] for (x, y) in combined_xy], float)
|
|
846
964
|
|
|
965
|
+
# Ground normals should point UP (+z) into the air
|
|
966
|
+
tris = _ensure_upward_normals(Vg, tris)
|
|
967
|
+
|
|
847
968
|
return {"ground_surface": (Vg, tris)}
|
|
848
969
|
|
|
849
970
|
def _half_width_at_depth(half_top: float, slope: float, top_z: float, z: float) -> float:
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# ABOUTME: Tests for SDF readiness: normal orientation and metadata export.
|
|
2
|
+
# ABOUTME: Ensures meshes are suitable for signed distance field computation.
|
|
3
|
+
"""
|
|
4
|
+
Tests verifying that generated meshes are ready for SDF computation.
|
|
5
|
+
|
|
6
|
+
These tests validate:
|
|
7
|
+
1. Normal orientation follows "into void" convention
|
|
8
|
+
2. SDF metadata is correctly exported
|
|
9
|
+
3. Trench opening polygon matches actual geometry
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
+
if str(ROOT) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(ROOT))
|
|
21
|
+
PKG_ROOT = ROOT / "packages"
|
|
22
|
+
if str(PKG_ROOT) not in sys.path:
|
|
23
|
+
sys.path.insert(0, str(PKG_ROOT))
|
|
24
|
+
VENVDIR = ROOT / ".venv"
|
|
25
|
+
if VENVDIR.exists():
|
|
26
|
+
for candidate in (VENVDIR / "lib").glob("python*/site-packages"):
|
|
27
|
+
if str(candidate) not in sys.path:
|
|
28
|
+
sys.path.insert(0, str(candidate))
|
|
29
|
+
break
|
|
30
|
+
|
|
31
|
+
from trenchfoot.generate_scenarios import (
|
|
32
|
+
ScenarioDefinition,
|
|
33
|
+
default_scenarios,
|
|
34
|
+
generate_scenarios,
|
|
35
|
+
)
|
|
36
|
+
from trenchfoot.trench_scene_generator_v3 import (
|
|
37
|
+
scene_spec_from_dict,
|
|
38
|
+
generate_surface_mesh,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _compute_face_normals(V: np.ndarray, F: np.ndarray) -> np.ndarray:
|
|
43
|
+
"""Compute per-face normals for a triangle mesh."""
|
|
44
|
+
p0 = V[F[:, 0]]
|
|
45
|
+
p1 = V[F[:, 1]]
|
|
46
|
+
p2 = V[F[:, 2]]
|
|
47
|
+
normals = np.cross(p1 - p0, p2 - p0)
|
|
48
|
+
# Normalize
|
|
49
|
+
lengths = np.linalg.norm(normals, axis=1, keepdims=True)
|
|
50
|
+
lengths = np.maximum(lengths, 1e-10) # Avoid division by zero
|
|
51
|
+
return normals / lengths
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _minimal_spec_dict() -> dict:
|
|
55
|
+
"""Minimal trench spec for testing."""
|
|
56
|
+
return {
|
|
57
|
+
"path_xy": [[0.0, 0.0], [3.0, 0.0]],
|
|
58
|
+
"width": 1.0,
|
|
59
|
+
"depth": 1.2,
|
|
60
|
+
"wall_slope": 0.1,
|
|
61
|
+
"ground": {"z0": 0.0, "slope": [0.0, 0.0], "size_margin": 2.0},
|
|
62
|
+
"pipes": [],
|
|
63
|
+
"boxes": [],
|
|
64
|
+
"spheres": [],
|
|
65
|
+
"noise": {"enable": False},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestFloorNormalOrientation:
|
|
70
|
+
"""Tests for trench floor normal orientation."""
|
|
71
|
+
|
|
72
|
+
def test_floor_normals_point_up_minimal(self):
|
|
73
|
+
"""Floor normals should point UP (+z) for correct SDF sign."""
|
|
74
|
+
spec = scene_spec_from_dict(_minimal_spec_dict())
|
|
75
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
76
|
+
|
|
77
|
+
V, F = result.groups["trench_bottom"]
|
|
78
|
+
normals = _compute_face_normals(V, F)
|
|
79
|
+
|
|
80
|
+
# All floor normals should have positive z component
|
|
81
|
+
z_components = normals[:, 2]
|
|
82
|
+
down_count = np.sum(z_components < 0)
|
|
83
|
+
|
|
84
|
+
assert down_count == 0, (
|
|
85
|
+
f"Floor has {down_count}/{len(F)} faces with downward normals. "
|
|
86
|
+
f"Floor normals should all point UP (+z) into the void."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Check they're strongly upward (nz > 0.5 for mostly-horizontal faces)
|
|
90
|
+
weak_up_count = np.sum(z_components < 0.5)
|
|
91
|
+
assert weak_up_count == 0, (
|
|
92
|
+
f"Floor has {weak_up_count}/{len(F)} faces with weak upward normals (nz < 0.5). "
|
|
93
|
+
f"Floor should be mostly horizontal."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@pytest.mark.parametrize("scenario_name", [
|
|
97
|
+
"S01_straight_vwalls",
|
|
98
|
+
"S02_straight_sloped",
|
|
99
|
+
"S03_angled_corner",
|
|
100
|
+
"S04_curved_bend",
|
|
101
|
+
"S05_pipe_cluster",
|
|
102
|
+
"S06_complex_layout",
|
|
103
|
+
"S07_circular_well",
|
|
104
|
+
])
|
|
105
|
+
def test_floor_normals_point_up_all_scenarios(self, scenario_name):
|
|
106
|
+
"""Floor normals should point UP (+z) for all standard scenarios."""
|
|
107
|
+
scenarios = default_scenarios()
|
|
108
|
+
scenario = next((s for s in scenarios if s.name == scenario_name), None)
|
|
109
|
+
if scenario is None:
|
|
110
|
+
pytest.skip(f"{scenario_name} not available")
|
|
111
|
+
|
|
112
|
+
spec = scene_spec_from_dict(scenario.spec)
|
|
113
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
114
|
+
|
|
115
|
+
V, F = result.groups["trench_bottom"]
|
|
116
|
+
normals = _compute_face_normals(V, F)
|
|
117
|
+
|
|
118
|
+
z_components = normals[:, 2]
|
|
119
|
+
down_count = np.sum(z_components < 0)
|
|
120
|
+
|
|
121
|
+
assert down_count == 0, (
|
|
122
|
+
f"{scenario_name}: Floor has {down_count}/{len(F)} faces with downward normals. "
|
|
123
|
+
f"All floor normals should point UP (+z)."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestWallNormalOrientation:
|
|
128
|
+
"""Tests for trench wall normal orientation."""
|
|
129
|
+
|
|
130
|
+
def test_wall_normals_point_into_void(self):
|
|
131
|
+
"""Wall normals should point INTO the trench void (toward centerline)."""
|
|
132
|
+
spec = scene_spec_from_dict(_minimal_spec_dict())
|
|
133
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
134
|
+
|
|
135
|
+
V, F = result.groups["trench_walls"]
|
|
136
|
+
normals = _compute_face_normals(V, F)
|
|
137
|
+
|
|
138
|
+
# For a straight trench along x-axis (y=0), walls are on either side.
|
|
139
|
+
# The trench centerline is at y=0.
|
|
140
|
+
# Left wall (y < 0) should have normals pointing +y (into trench)
|
|
141
|
+
# Right wall (y > 0) should have normals pointing -y (into trench)
|
|
142
|
+
|
|
143
|
+
# Compute face centroids
|
|
144
|
+
centroids = (V[F[:, 0]] + V[F[:, 1]] + V[F[:, 2]]) / 3
|
|
145
|
+
|
|
146
|
+
# Check each face: direction from centroid to centerline should align with normal
|
|
147
|
+
errors = 0
|
|
148
|
+
for i, (centroid, normal) in enumerate(zip(centroids, normals)):
|
|
149
|
+
# Direction toward centerline (y=0)
|
|
150
|
+
toward_center_y = -np.sign(centroid[1]) if abs(centroid[1]) > 0.01 else 0
|
|
151
|
+
|
|
152
|
+
# For wall faces, the y-component of normal should match direction toward center
|
|
153
|
+
if abs(normal[1]) > 0.3: # Face has significant y-normal component
|
|
154
|
+
if np.sign(normal[1]) != toward_center_y:
|
|
155
|
+
errors += 1
|
|
156
|
+
|
|
157
|
+
assert errors == 0, (
|
|
158
|
+
f"Wall has {errors}/{len(F)} faces with normals pointing away from trench void. "
|
|
159
|
+
f"Wall normals should point inward toward the trench centerline."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize("scenario_name", [
|
|
163
|
+
"S01_straight_vwalls",
|
|
164
|
+
"S02_straight_sloped",
|
|
165
|
+
"S03_angled_corner",
|
|
166
|
+
])
|
|
167
|
+
def test_wall_normals_mostly_horizontal(self, scenario_name):
|
|
168
|
+
"""Wall normals should be mostly horizontal (small z component)."""
|
|
169
|
+
scenarios = default_scenarios()
|
|
170
|
+
scenario = next((s for s in scenarios if s.name == scenario_name), None)
|
|
171
|
+
if scenario is None:
|
|
172
|
+
pytest.skip(f"{scenario_name} not available")
|
|
173
|
+
|
|
174
|
+
spec = scene_spec_from_dict(scenario.spec)
|
|
175
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
176
|
+
|
|
177
|
+
V, F = result.groups["trench_walls"]
|
|
178
|
+
normals = _compute_face_normals(V, F)
|
|
179
|
+
|
|
180
|
+
# Wall faces should have mostly horizontal normals
|
|
181
|
+
# Allow some z component for sloped walls but should be < 0.7
|
|
182
|
+
z_components = np.abs(normals[:, 2])
|
|
183
|
+
vertical_wall_ratio = np.mean(z_components < 0.7)
|
|
184
|
+
|
|
185
|
+
assert vertical_wall_ratio > 0.8, (
|
|
186
|
+
f"{scenario_name}: Only {vertical_wall_ratio*100:.1f}% of wall faces have |nz| < 0.7. "
|
|
187
|
+
f"Wall faces should be mostly vertical (horizontal normals)."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TestGroundNormalOrientation:
|
|
192
|
+
"""Tests for ground surface normal orientation."""
|
|
193
|
+
|
|
194
|
+
def test_ground_normals_point_up(self):
|
|
195
|
+
"""Ground surface normals should point UP (+z)."""
|
|
196
|
+
spec = scene_spec_from_dict(_minimal_spec_dict())
|
|
197
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
198
|
+
|
|
199
|
+
V, F = result.groups["ground_surface"]
|
|
200
|
+
normals = _compute_face_normals(V, F)
|
|
201
|
+
|
|
202
|
+
z_components = normals[:, 2]
|
|
203
|
+
down_count = np.sum(z_components < 0)
|
|
204
|
+
|
|
205
|
+
assert down_count == 0, (
|
|
206
|
+
f"Ground has {down_count}/{len(F)} faces with downward normals. "
|
|
207
|
+
f"Ground normals should all point UP (+z)."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class TestSDFMetadata:
|
|
212
|
+
"""Tests for SDF metadata export."""
|
|
213
|
+
|
|
214
|
+
def test_metadata_exported(self, tmp_path):
|
|
215
|
+
"""SDF metadata JSON should be exported alongside mesh."""
|
|
216
|
+
spec = scene_spec_from_dict(_minimal_spec_dict())
|
|
217
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
218
|
+
files = result.persist(tmp_path)
|
|
219
|
+
|
|
220
|
+
assert files.sdf_metadata_path is not None, "sdf_metadata_path should be set"
|
|
221
|
+
assert files.sdf_metadata_path.exists(), "sdf_metadata.json should exist"
|
|
222
|
+
|
|
223
|
+
def test_metadata_has_required_fields(self, tmp_path):
|
|
224
|
+
"""SDF metadata should contain all required fields."""
|
|
225
|
+
spec = scene_spec_from_dict(_minimal_spec_dict())
|
|
226
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
227
|
+
files = result.persist(tmp_path)
|
|
228
|
+
|
|
229
|
+
with files.sdf_metadata_path.open() as f:
|
|
230
|
+
data = json.load(f)
|
|
231
|
+
|
|
232
|
+
assert "sdf_metadata" in data, "Root key 'sdf_metadata' missing"
|
|
233
|
+
meta = data["sdf_metadata"]
|
|
234
|
+
|
|
235
|
+
required_fields = ["version", "normal_convention", "geometry_type", "trench_opening"]
|
|
236
|
+
for field in required_fields:
|
|
237
|
+
assert field in meta, f"Required field '{field}' missing from metadata"
|
|
238
|
+
|
|
239
|
+
assert meta["version"] == "1.0"
|
|
240
|
+
assert meta["normal_convention"] == "into_void"
|
|
241
|
+
assert meta["geometry_type"] == "open_trench"
|
|
242
|
+
|
|
243
|
+
def test_metadata_trench_opening_structure(self, tmp_path):
|
|
244
|
+
"""Trench opening should have proper polygon structure."""
|
|
245
|
+
spec = scene_spec_from_dict(_minimal_spec_dict())
|
|
246
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
247
|
+
files = result.persist(tmp_path)
|
|
248
|
+
|
|
249
|
+
with files.sdf_metadata_path.open() as f:
|
|
250
|
+
data = json.load(f)
|
|
251
|
+
|
|
252
|
+
opening = data["sdf_metadata"]["trench_opening"]
|
|
253
|
+
assert opening["type"] == "polygon"
|
|
254
|
+
assert "vertices_xy" in opening
|
|
255
|
+
assert "z_level" in opening
|
|
256
|
+
|
|
257
|
+
vertices = opening["vertices_xy"]
|
|
258
|
+
assert len(vertices) >= 4, "Trench opening should have at least 4 vertices"
|
|
259
|
+
|
|
260
|
+
# Each vertex should be [x, y]
|
|
261
|
+
for v in vertices:
|
|
262
|
+
assert len(v) == 2, f"Vertex should be [x, y], got {v}"
|
|
263
|
+
|
|
264
|
+
def test_metadata_polygon_bounds_match_trench(self, tmp_path):
|
|
265
|
+
"""Trench opening polygon should match actual trench geometry bounds."""
|
|
266
|
+
spec_dict = _minimal_spec_dict()
|
|
267
|
+
spec = scene_spec_from_dict(spec_dict)
|
|
268
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
269
|
+
files = result.persist(tmp_path)
|
|
270
|
+
|
|
271
|
+
with files.sdf_metadata_path.open() as f:
|
|
272
|
+
data = json.load(f)
|
|
273
|
+
|
|
274
|
+
opening = data["sdf_metadata"]["trench_opening"]
|
|
275
|
+
vertices = np.array(opening["vertices_xy"])
|
|
276
|
+
|
|
277
|
+
# The trench path goes from [0,0] to [3,0] with width 1.0
|
|
278
|
+
# The metadata uses the base half-width (0.5), which represents the
|
|
279
|
+
# footprint of the trench floor, not the sloped width at z=0.
|
|
280
|
+
expected_half_width = 0.5
|
|
281
|
+
|
|
282
|
+
# Check x range covers path (0 to 3)
|
|
283
|
+
x_min, x_max = vertices[:, 0].min(), vertices[:, 0].max()
|
|
284
|
+
assert x_min <= 0.1, f"Opening x_min ({x_min}) should be near 0"
|
|
285
|
+
assert x_max >= 2.9, f"Opening x_max ({x_max}) should be near 3"
|
|
286
|
+
|
|
287
|
+
# Check y range matches half-width
|
|
288
|
+
y_min, y_max = vertices[:, 1].min(), vertices[:, 1].max()
|
|
289
|
+
assert abs(y_min + expected_half_width) < 0.1, (
|
|
290
|
+
f"Opening y_min ({y_min}) should be near {-expected_half_width}"
|
|
291
|
+
)
|
|
292
|
+
assert abs(y_max - expected_half_width) < 0.1, (
|
|
293
|
+
f"Opening y_max ({y_max}) should be near {expected_half_width}"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def test_metadata_surface_groups_present(self, tmp_path):
|
|
297
|
+
"""Surface groups should list all mesh groups with properties."""
|
|
298
|
+
spec = scene_spec_from_dict(_minimal_spec_dict())
|
|
299
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
300
|
+
files = result.persist(tmp_path)
|
|
301
|
+
|
|
302
|
+
with files.sdf_metadata_path.open() as f:
|
|
303
|
+
data = json.load(f)
|
|
304
|
+
|
|
305
|
+
groups = data["sdf_metadata"]["surface_groups"]
|
|
306
|
+
|
|
307
|
+
expected_groups = ["trench_bottom", "trench_walls", "ground_surface"]
|
|
308
|
+
for group in expected_groups:
|
|
309
|
+
assert group in groups, f"Expected surface group '{group}' in metadata"
|
|
310
|
+
assert "normal_direction" in groups[group]
|
|
311
|
+
assert "surface_type" in groups[group]
|
|
312
|
+
|
|
313
|
+
# Verify correct normal conventions documented
|
|
314
|
+
assert groups["trench_bottom"]["normal_direction"] == "up"
|
|
315
|
+
assert groups["trench_walls"]["normal_direction"] == "inward"
|
|
316
|
+
assert groups["ground_surface"]["normal_direction"] == "up"
|
|
317
|
+
|
|
318
|
+
def test_metadata_embedded_objects_count(self, tmp_path):
|
|
319
|
+
"""Embedded objects count should match spec."""
|
|
320
|
+
spec_dict = _minimal_spec_dict()
|
|
321
|
+
# Note: PipeSpec uses s_center, SphereSpec uses s
|
|
322
|
+
spec_dict["pipes"] = [
|
|
323
|
+
{"radius": 0.1, "length": 1.0, "angle_deg": 0, "s_center": 0.5, "z": -0.6, "offset_u": 0},
|
|
324
|
+
{"radius": 0.05, "length": 0.8, "angle_deg": 90, "s_center": 0.3, "z": -0.4, "offset_u": 0},
|
|
325
|
+
]
|
|
326
|
+
spec_dict["spheres"] = [{"radius": 0.15, "s": 0.7, "z": -0.5}]
|
|
327
|
+
|
|
328
|
+
spec = scene_spec_from_dict(spec_dict)
|
|
329
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
330
|
+
files = result.persist(tmp_path)
|
|
331
|
+
|
|
332
|
+
with files.sdf_metadata_path.open() as f:
|
|
333
|
+
data = json.load(f)
|
|
334
|
+
|
|
335
|
+
embedded = data["sdf_metadata"]["embedded_objects"]
|
|
336
|
+
assert embedded["pipes"] == 2
|
|
337
|
+
assert embedded["spheres"] == 1
|
|
338
|
+
assert embedded["boxes"] == 0
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class TestCircularWellNormals:
|
|
342
|
+
"""Specific tests for circular well (S07) normal orientation."""
|
|
343
|
+
|
|
344
|
+
def test_s07_floor_normals_all_upward(self):
|
|
345
|
+
"""S07 circular well floor should have 100% upward normals."""
|
|
346
|
+
scenarios = default_scenarios()
|
|
347
|
+
s07 = next((s for s in scenarios if s.name == "S07_circular_well"), None)
|
|
348
|
+
if s07 is None:
|
|
349
|
+
pytest.skip("S07_circular_well not available")
|
|
350
|
+
|
|
351
|
+
spec = scene_spec_from_dict(s07.spec)
|
|
352
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
353
|
+
|
|
354
|
+
V, F = result.groups["trench_bottom"]
|
|
355
|
+
normals = _compute_face_normals(V, F)
|
|
356
|
+
|
|
357
|
+
z_components = normals[:, 2]
|
|
358
|
+
down_count = np.sum(z_components < 0)
|
|
359
|
+
up_pct = 100 * (1 - down_count / len(F))
|
|
360
|
+
|
|
361
|
+
assert down_count == 0, (
|
|
362
|
+
f"S07 circular well has {down_count}/{len(F)} floor faces with downward normals "
|
|
363
|
+
f"({up_pct:.1f}% upward). Should be 100% upward."
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def test_s07_annulus_ground_normals_upward(self):
|
|
367
|
+
"""S07 ground annulus should have upward normals."""
|
|
368
|
+
scenarios = default_scenarios()
|
|
369
|
+
s07 = next((s for s in scenarios if s.name == "S07_circular_well"), None)
|
|
370
|
+
if s07 is None:
|
|
371
|
+
pytest.skip("S07_circular_well not available")
|
|
372
|
+
|
|
373
|
+
spec = scene_spec_from_dict(s07.spec)
|
|
374
|
+
result = generate_surface_mesh(spec, make_preview=False)
|
|
375
|
+
|
|
376
|
+
V, F = result.groups["ground_surface"]
|
|
377
|
+
normals = _compute_face_normals(V, F)
|
|
378
|
+
|
|
379
|
+
z_components = normals[:, 2]
|
|
380
|
+
down_count = np.sum(z_components < 0)
|
|
381
|
+
|
|
382
|
+
assert down_count == 0, (
|
|
383
|
+
f"S07 ground annulus has {down_count}/{len(F)} faces with downward normals. "
|
|
384
|
+
f"Ground should all point UP."
|
|
385
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/metrics.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/preview.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S01_straight_vwalls/scene.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/metrics.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S05_wide_slope_pair/scene.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/metrics.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S06_bumpy_wide_loop/scene.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/metrics.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{trenchfoot-0.2.7 → trenchfoot-0.3.0}/packages/trenchfoot/scenarios/S07_circular_well/scene.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|