Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions cadling/cadling/lib/geometry/uv_grid_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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([
Expand Down
2 changes: 1 addition & 1 deletion cadling/cadling/lib/topology/face_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
37 changes: 37 additions & 0 deletions cadling/tests/unit/lib/geometry/test_uv_grid_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 35 additions & 0 deletions cadling/tests/unit/lib/topology/test_face_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading