Spaces:
Sleeping
Sleeping
File size: 6,582 Bytes
182efca | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | """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
|