"""G0/G1/G2 surface continuity checking at boundary regions.""" from __future__ import annotations import logging from dataclasses import dataclass, field import numpy as np from OCP.BRep import BRep_Tool from OCP.BRepAdaptor import BRepAdaptor_Surface from OCP.GeomLProp import GeomLProp_SLProps from OCP.ShapeAnalysis import ShapeAnalysis_Surface from OCP.TopAbs import TopAbs_FACE from OCP.TopExp import TopExp_Explorer from OCP.TopoDS import TopoDS, TopoDS_Face, TopoDS_Shape from OCP.gp import gp_Pnt logger = logging.getLogger(__name__) @dataclass class ContinuityResult: """Results of continuity checks.""" g0_deviations: list[float] = field(default_factory=list) g1_deviations_deg: list[float] = field(default_factory=list) g2_deviations_pct: list[float] = field(default_factory=list) g0_max: float = 0.0 g1_max_deg: float = 0.0 g2_max_pct: float = 0.0 g0_pass: bool = True g1_pass: bool = True g2_pass: bool = True num_samples_checked: int = 0 def _get_closest_face_and_uv( faces: list[TopoDS_Face], point: np.ndarray, ) -> tuple[TopoDS_Face, float, float, float] | None: """Find the closest face to a 3D point and return UV parameters.""" best_face = None best_dist = float("inf") best_u, best_v = 0.0, 0.0 gp_point = gp_Pnt(float(point[0]), float(point[1]), float(point[2])) for face in faces: try: surface = BRep_Tool.Surface_s(face) sas = ShapeAnalysis_Surface(surface) uv = sas.ValueOfUV(gp_point, 1.0) proj_pnt = sas.Value(uv.X(), uv.Y()) dist = gp_point.Distance(proj_pnt) if dist < best_dist: best_dist = dist best_face = face best_u = uv.X() best_v = uv.Y() except Exception: continue if best_face is None: return None return best_face, best_u, best_v, best_dist def _evaluate_surface_props( face: TopoDS_Face, u: float, v: float, ) -> tuple[np.ndarray | None, np.ndarray | None, float | None]: """Evaluate surface normal and curvature at (u, v).""" try: surface = BRep_Tool.Surface_s(face) props = GeomLProp_SLProps(surface, u, v, 2, 1e-6) normal = None if props.IsNormalDefined(): n = props.Normal() normal = np.array([n.X(), n.Y(), n.Z()]) curvature = None if props.IsCurvatureDefined(): curvature = props.MeanCurvature() point = props.Value() position = np.array([point.X(), point.Y(), point.Z()]) return position, normal, curvature except Exception: return None, None, None def _collect_faces(shape: TopoDS_Shape) -> list[TopoDS_Face]: """Collect all faces from a shape.""" faces = [] exp = TopExp_Explorer(shape, TopAbs_FACE) while exp.More(): faces.append(TopoDS.Face_s(exp.Current())) exp.Next() return faces def check_continuity( shape_a: TopoDS_Shape, shape_b: TopoDS_Shape, sample_points: np.ndarray, g0_tolerance_mm: float = 0.05, g1_tolerance_deg: float = 0.3, g2_tolerance_pct: float = 3.0, max_check_points: int = 1000, ) -> ContinuityResult: """Check G0/G1/G2 continuity between two shapes at sample points. Args: shape_a: First shape (reference). shape_b: Second shape (comparison). sample_points: Points at which to evaluate continuity. g0_tolerance_mm: G0 positional tolerance. g1_tolerance_deg: G1 tangent angle tolerance. g2_tolerance_pct: G2 curvature relative tolerance. max_check_points: Limit on number of points to check. Returns: ContinuityResult with pass/fail per grade. """ faces_a = _collect_faces(shape_a) faces_b = _collect_faces(shape_b) if not faces_a or not faces_b: logger.warning("One or both shapes have no faces -- skipping continuity check") return ContinuityResult() # Subsample if needed check_points = sample_points if len(sample_points) > max_check_points: rng = np.random.default_rng(42) indices = rng.choice(len(sample_points), max_check_points, replace=False) check_points = sample_points[indices] result = ContinuityResult() for point in check_points: match_a = _get_closest_face_and_uv(faces_a, point) match_b = _get_closest_face_and_uv(faces_b, point) if match_a is None or match_b is None: continue face_a, u_a, v_a, dist_a = match_a face_b, u_b, v_b, dist_b = match_b # Skip if projection is too far (point not on either surface) if dist_a > g0_tolerance_mm * 10 or dist_b > g0_tolerance_mm * 10: continue pos_a, norm_a, curv_a = _evaluate_surface_props(face_a, u_a, v_a) pos_b, norm_b, curv_b = _evaluate_surface_props(face_b, u_b, v_b) if pos_a is None or pos_b is None: continue result.num_samples_checked += 1 # G0: positional g0_dev = float(np.linalg.norm(pos_a - pos_b)) result.g0_deviations.append(g0_dev) # G1: tangent (normal angle) if norm_a is not None and norm_b is not None: cos_angle = np.clip(np.dot(norm_a, norm_b), -1.0, 1.0) angle_deg = float(np.degrees(np.arccos(abs(cos_angle)))) result.g1_deviations_deg.append(angle_deg) # G2: curvature if curv_a is not None and curv_b is not None: denom = max(abs(curv_a), abs(curv_b), 1e-12) g2_pct = abs(curv_a - curv_b) / denom * 100.0 result.g2_deviations_pct.append(float(g2_pct)) # Compute max and pass/fail if result.g0_deviations: result.g0_max = max(result.g0_deviations) result.g0_pass = result.g0_max <= g0_tolerance_mm if result.g1_deviations_deg: result.g1_max_deg = max(result.g1_deviations_deg) result.g1_pass = result.g1_max_deg <= g1_tolerance_deg if result.g2_deviations_pct: result.g2_max_pct = max(result.g2_deviations_pct) result.g2_pass = result.g2_max_pct <= g2_tolerance_pct logger.info( "Continuity check (%d points): G0 max=%.4f mm (%s), " "G1 max=%.2f deg (%s), G2 max=%.1f%% (%s)", result.num_samples_checked, result.g0_max, "PASS" if result.g0_pass else "FAIL", result.g1_max_deg, "PASS" if result.g1_pass else "FAIL", result.g2_max_pct, "PASS" if result.g2_pass else "FAIL", ) return result