diff --git a/cadling/cadling/lib/geometry/uv_grid_extractor.py b/cadling/cadling/lib/geometry/uv_grid_extractor.py index df6c617..e51c25a 100644 --- a/cadling/cadling/lib/geometry/uv_grid_extractor.py +++ b/cadling/cadling/lib/geometry/uv_grid_extractor.py @@ -21,7 +21,6 @@ # Try importing occwl; if not on sys.path, try adding the vendored location try: from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge - from OCC.Core.TopAbs import TopAbs_IN, TopAbs_OUT from occwl.face import Face from occwl.edge import Edge from occwl.uvgrid import uvgrid, ugrid @@ -33,7 +32,6 @@ sys.path.insert(0, str(_occwl_path)) try: from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge - from OCC.Core.TopAbs import TopAbs_IN, TopAbs_OUT from occwl.face import Face from occwl.edge import Edge from occwl.uvgrid import uvgrid, ugrid @@ -99,19 +97,17 @@ def extract_uv_grid( logger.warning("Failed to extract normal grid from face") return None - # Compute trimming mask - # Get UV values where we sampled - _, uv_values = uvgrid(face, num_u=num_u, num_v=num_v, method="point", uvs=True) - - # Create trimming mask by checking if each UV point is inside the trimmed region - trimming_mask = np.zeros((num_u, num_v, 1), dtype=np.float32) - for i in range(num_u): - for j in range(num_v): - uv = uv_values[i, j] - # Use visibility_status: 0=IN, 1=OUT, 2=ON, 3=UNKNOWN - status = face.visibility_status(uv) - # Mark as 1 if inside (TopAbs_IN = 0) - trimming_mask[i, j, 0] = 1.0 if status == TopAbs_IN else 0.0 + # Compute trimming mask via occwl's "inside" sampling, which returns + # a [num_u, num_v, 1] boolean grid (1 where the sample lies inside + # the face's trimmed region). This replaces a per-point + # face.visibility_status(uv) loop, which raised a gp_Pnt2d SWIG + # overload error when handed numpy-float UV coordinates on some + # pythonocc/occwl builds and made every face UV-grid fail. + inside_grid = uvgrid(face, num_u=num_u, num_v=num_v, method="inside") + if inside_grid is None: + logger.warning("Failed to extract trimming mask from face") + return None + trimming_mask = inside_grid.astype(np.float32) # Stack into [num_u, num_v, 7] array uv_grid = np.concatenate([ diff --git a/cadling/cadling/lib/topology/face_identity.py b/cadling/cadling/lib/topology/face_identity.py index fd910fa..290f701 100644 --- a/cadling/cadling/lib/topology/face_identity.py +++ b/cadling/cadling/lib/topology/face_identity.py @@ -39,7 +39,7 @@ from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face, TopoDS_Edge, TopoDS_Vertex from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE, TopAbs_VERTEX - from OCC.Core import topods + from OCC.Core.TopoDS import topods HAS_OCC = True except ImportError: diff --git a/cadling/tests/unit/lib/geometry/test_uv_grid_extractor.py b/cadling/tests/unit/lib/geometry/test_uv_grid_extractor.py index e5cac15..86022f5 100644 --- a/cadling/tests/unit/lib/geometry/test_uv_grid_extractor.py +++ b/cadling/tests/unit/lib/geometry/test_uv_grid_extractor.py @@ -266,3 +266,40 @@ def test_edge_grid_sizes(self, real_occ_edges): if result is not None: assert result.shape == (num_u, 6) assert np.all(np.isfinite(result)) + + +class TestFaceUVGridRegression: + """Regression: face UV-grid extraction must SUCCEED on a real face. + + Before the fix, the trimming-mask computation called + ``face.visibility_status(uv)`` with numpy-float UV coordinates, which + raised a ``gp_Pnt2d`` SWIG overload error on pythonocc/occwl; the broad + ``except`` swallowed it and ``extract_uv_grid`` returned ``None`` for + *every* face. The other tests here guard their assertions with + ``if result is not None:`` and so passed vacuously, hiding the failure. + This test asserts success unconditionally on a self-contained box face. + """ + + @pytest.mark.requires_pythonocc + def test_box_face_uv_grid_succeeds(self): + pytest.importorskip("OCC") + pytest.importorskip("occwl") + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox + from OCC.Core.TopAbs import TopAbs_FACE + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + + box = BRepPrimAPI_MakeBox(10.0, 10.0, 10.0).Shape() + explorer = TopExp_Explorer(box, TopAbs_FACE) + face = topods.Face(explorer.Current()) + + result = FaceUVGridExtractor.extract_uv_grid(face, num_u=10, num_v=10) + assert result is not None, "face UV-grid extraction must succeed (gp_Pnt2d fix)" + assert result.shape == (10, 10, 7) + assert np.all(np.isfinite(result)) + + trimming = result[:, :, 6] + assert np.all((trimming == 0) | (trimming == 1)) + # A planar box face is fully inside its trimmed region: real material + # is sampled (the mask was impossible to populate before the fix). + assert trimming.sum() > 0 diff --git a/cadling/tests/unit/lib/topology/test_face_identity.py b/cadling/tests/unit/lib/topology/test_face_identity.py index ce4ee22..f5beddf 100644 --- a/cadling/tests/unit/lib/topology/test_face_identity.py +++ b/cadling/tests/unit/lib/topology/test_face_identity.py @@ -353,3 +353,38 @@ def test_register_all_vertices_no_occ(self): result = registry.register_all_vertices(mock_shape) assert result == {} + + +class TestShapeIdentityRegistryRealShape: + """Regression: register_all must populate from a REAL OCC shape. + + Before the fix, face_identity imported ``from OCC.Core import topods``, + which raises ``ImportError`` on pythonocc 7.8 ("cannot import name + 'topods' from 'OCC.Core'"). That set ``HAS_OCC = False`` at module load, + so ``register_all_*`` registered nothing and every face/edge index was -1. + Every other test in this file mocks the shapes, so the real-OCC path was + never exercised and the bug went unnoticed. + """ + + @pytest.mark.requires_pythonocc + def test_register_all_from_real_box(self): + pytest.importorskip("OCC") + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox + + from cadling.lib.topology.face_identity import HAS_OCC, ShapeIdentityRegistry + + assert HAS_OCC, "face_identity should detect pythonocc (topods import)" + + box = BRepPrimAPI_MakeBox(10.0, 10.0, 10.0).Shape() + registry = ShapeIdentityRegistry() + registry.register_all(box) + + # A box solid has 6 faces, 12 edges, 8 vertices. + assert registry.num_faces == 6 + assert registry.num_edges == 12 + assert registry.num_vertices == 8 + + # Index <-> id <-> shape round-trips for a registered face. + face_id, face = registry.get_face_by_index(0) + assert registry.get_face_index(face_id) == 0 + assert registry.get_face(face_id) is face