"""n3xd_ocp.measure — the batched face measurement. Checked against the same per-face reference the Inc 1 gate uses, which is recorded from the stock wheel: the batched scan has to agree with calling BRepGProp.SurfaceProperties once per face, in the same MapShapes(FACE) order, or the anchor path would resolve against different numbers. """ from __future__ import annotations import pytest np = pytest.importorskip("numpy") import n3xd_ocp from OCP.BRepGProp import BRepGProp from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox from OCP.GProp import GProp_GProps from OCP.TopAbs import TopAbs_FACE from .test_inc1_modeling import sub_shapes def _per_face(shape, eps=None): """The one-call-per-face loop this API exists to replace.""" rows = [] for face in sub_shapes(shape, TopAbs_FACE): props = GProp_GProps() if eps is None: BRepGProp.SurfaceProperties_s(face, props) else: BRepGProp.SurfaceProperties_s(face, props, eps) centre = props.CentreOfMass() rows.append((props.Mass(), (centre.X(), centre.Y(), centre.Z()))) return rows def test_matches_the_stock_reference(inc1_or_skip, fixture_shapes): for name, expected in inc1_or_skip["face_props"].items(): areas, centroids = n3xd_ocp.measure.face_surface_props(fixture_shapes[name]) assert areas.shape == (len(expected),), name assert centroids.shape == (len(expected), 3), name for i, row in enumerate(expected): assert areas[i] == pytest.approx(row[0], rel=1e-9), f"{name}[{i}].area" assert centroids[i] == pytest.approx(row[1:], rel=1e-9, abs=1e-12), ( f"{name}[{i}].centroid" ) def test_matches_the_per_face_loop(fixture_shapes): shape = fixture_shapes["cut_cylinder"] areas, centroids = n3xd_ocp.measure.face_surface_props(shape) for i, (area, centre) in enumerate(_per_face(shape)): assert areas[i] == pytest.approx(area, rel=1e-12) assert centroids[i] == pytest.approx(centre, rel=1e-12, abs=1e-12) def test_parallel_and_serial_agree_exactly(fixture_shapes): """The parallel path shares one TopoDS_Shape across threads. If OCCT mutated anything reachable from it while measuring, this is where it would show — so the comparison is exact, not approximate.""" shape = fixture_shapes["fused"] par = n3xd_ocp.measure.face_surface_props(shape, parallel=True) ser = n3xd_ocp.measure.face_surface_props(shape, parallel=False) assert np.array_equal(par[0], ser[0]) assert np.array_equal(par[1], ser[1]) def test_eps_overload_is_reachable(fixture_shapes): shape = fixture_shapes["box"] areas, _ = n3xd_ocp.measure.face_surface_props(shape, eps=1e-6) for i, (area, _centre) in enumerate(_per_face(shape, eps=1e-6)): assert areas[i] == pytest.approx(area, rel=1e-12) def test_empty_shape_returns_empty_arrays(fixture_shapes): areas, centroids = n3xd_ocp.measure.face_surface_props( fixture_shapes["empty_compound"] ) assert areas.shape == (0,) assert centroids.shape == (0, 3) def test_arrays_own_their_memory(fixture_shapes): """The buffers are allocated C++-side and handed over with a capsule deleter; the arrays must stay valid once every other reference is gone.""" import gc areas, centroids = n3xd_ocp.measure.face_surface_props(fixture_shapes["box"]) expected = float(areas.sum()) gc.collect() assert float(areas.sum()) == expected assert centroids.base is not None # the capsule, not the shape def test_survives_the_shape_being_dropped(): """Nothing in the result points back into the shape.""" areas, centroids = n3xd_ocp.measure.face_surface_props( BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() ) assert areas.sum() == pytest.approx(2200.0) assert centroids.shape == (6, 3)