Spaces:
Sleeping
Sleeping
| """Gap and overlap detection on boundary edges.""" | |
| from __future__ import annotations | |
| import logging | |
| from dataclasses import dataclass, field | |
| import numpy as np | |
| import open3d as o3d | |
| from OCP.BRep import BRep_Tool | |
| from OCP.BRepAdaptor import BRepAdaptor_Curve | |
| from OCP.GCPnts import GCPnts_UniformAbscissa | |
| from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE | |
| from OCP.TopExp import TopExp_Explorer | |
| from OCP.TopoDS import TopoDS, TopoDS_Shape | |
| logger = logging.getLogger(__name__) | |
| NUM_EDGE_SAMPLES = 50 | |
| class GapOverlapResult: | |
| """Gap and overlap detection results.""" | |
| gap_distances: list[float] = field(default_factory=list) | |
| overlap_distances: list[float] = field(default_factory=list) | |
| gap_locations: list[np.ndarray] = field(default_factory=list) | |
| overlap_locations: list[np.ndarray] = field(default_factory=list) | |
| def gap_count(self) -> int: | |
| return len(self.gap_distances) | |
| def overlap_count(self) -> int: | |
| return len(self.overlap_distances) | |
| def max_gap(self) -> float: | |
| return max(self.gap_distances) if self.gap_distances else 0.0 | |
| def max_overlap(self) -> float: | |
| return max(self.overlap_distances) if self.overlap_distances else 0.0 | |
| def _sample_boundary_edges(shape: TopoDS_Shape) -> np.ndarray: | |
| """Sample points along boundary edges of a shape. | |
| Boundary edges are edges that belong to only one face. | |
| """ | |
| # Collect edge-to-face counts | |
| edge_face_count: dict[int, int] = {} | |
| edge_map: dict[int, object] = {} | |
| face_exp = TopExp_Explorer(shape, TopAbs_FACE) | |
| while face_exp.More(): | |
| face = TopoDS.Face_s(face_exp.Current()) | |
| edge_exp = TopExp_Explorer(face, TopAbs_EDGE) | |
| while edge_exp.More(): | |
| edge = TopoDS.Edge_s(edge_exp.Current()) | |
| h = hash(edge) | |
| edge_face_count[h] = edge_face_count.get(h, 0) + 1 | |
| edge_map[h] = edge | |
| edge_exp.Next() | |
| face_exp.Next() | |
| # Boundary edges: shared by only one face | |
| boundary_edges = [edge_map[h] for h, count in edge_face_count.items() if count == 1] | |
| logger.info("Found %d boundary edges out of %d total", len(boundary_edges), len(edge_map)) | |
| if not boundary_edges: | |
| return np.empty((0, 3), dtype=np.float64) | |
| points = [] | |
| for edge in boundary_edges: | |
| try: | |
| curve = BRepAdaptor_Curve(edge) | |
| u_first = curve.FirstParameter() | |
| u_last = curve.LastParameter() | |
| distributor = GCPnts_UniformAbscissa(curve, NUM_EDGE_SAMPLES, u_first, u_last) | |
| if not distributor.IsDone(): | |
| continue | |
| for i in range(1, distributor.NbPoints() + 1): | |
| param = distributor.Parameter(i) | |
| pnt = curve.Value(param) | |
| points.append([pnt.X(), pnt.Y(), pnt.Z()]) | |
| except Exception: | |
| continue | |
| if not points: | |
| return np.empty((0, 3), dtype=np.float64) | |
| return np.array(points, dtype=np.float64) | |
| def detect_gaps_and_overlaps( | |
| shape_a: TopoDS_Shape, | |
| shape_b: TopoDS_Shape, | |
| vertices_b: np.ndarray, | |
| gap_tolerance_mm: float = 0.05, | |
| overlap_tolerance_mm: float = 0.05, | |
| ) -> GapOverlapResult: | |
| """Detect gaps and overlaps by projecting boundary edge points. | |
| Samples boundary edges of shape_a and measures distance to | |
| the closest point on shape_b's mesh surface. | |
| Args: | |
| shape_a: First shape (will sample boundary edges from). | |
| shape_b: Second shape (reference). | |
| vertices_b: Tessellated vertices of shape_b. | |
| gap_tolerance_mm: Gap detection threshold. | |
| overlap_tolerance_mm: Overlap detection threshold. | |
| Returns: | |
| GapOverlapResult with detected gaps and overlaps. | |
| """ | |
| boundary_points = _sample_boundary_edges(shape_a) | |
| if len(boundary_points) == 0: | |
| logger.info("No boundary edges found -- skipping gap/overlap detection") | |
| return GapOverlapResult() | |
| if len(vertices_b) == 0: | |
| logger.warning("Target mesh has no vertices -- skipping gap/overlap detection") | |
| return GapOverlapResult() | |
| # Build KDTree on target mesh | |
| target_pcd = o3d.geometry.PointCloud() | |
| target_pcd.points = o3d.utility.Vector3dVector(vertices_b) | |
| kdtree = o3d.geometry.KDTreeFlann(target_pcd) | |
| result = GapOverlapResult() | |
| for point in boundary_points: | |
| _, idx, dist_sq = kdtree.search_knn_vector_3d(point, 1) | |
| dist = np.sqrt(dist_sq[0]) | |
| if dist > gap_tolerance_mm: | |
| result.gap_distances.append(float(dist)) | |
| result.gap_locations.append(point) | |
| elif dist < overlap_tolerance_mm * 0.1: | |
| # Very close = potential overlap (surface interpenetration) | |
| result.overlap_distances.append(float(dist)) | |
| result.overlap_locations.append(point) | |
| logger.info( | |
| "Gap/overlap detection: %d gaps (max=%.4f mm), %d overlaps (max=%.4f mm)", | |
| result.gap_count, result.max_gap, | |
| result.overlap_count, result.max_overlap, | |
| ) | |
| return result | |