"""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 @dataclass 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) @property def gap_count(self) -> int: return len(self.gap_distances) @property def overlap_count(self) -> int: return len(self.overlap_distances) @property def max_gap(self) -> float: return max(self.gap_distances) if self.gap_distances else 0.0 @property 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