"""Inc 1 gate: core modeling reproduces the stock wheel's answers. The app's own suite cannot run until the last module is bound (its conftest imports n3xd.main, i.e. the whole OCP surface), so an increment is gated on the reference block tools/gen_fixtures.py records from the stock wheel. What is compared, and why in that form: * counts and boolean history maps — **exactly**. They are topological, so no tolerance is meaningful, and the history *is* the topological-naming substrate: the app decides which feature owns which face from it. * measurements — at a relative tolerance. Same kernel and same conservative optimiser flags, so they agree to far better than this; the tolerance is there so a last-ulp difference is not reported as a parity failure. * constructed shapes — by BREP digest, which is the strongest available statement and the one cad/derive.py's content addressing depends on. """ from __future__ import annotations import hashlib import io import pytest from OCP.Bnd import Bnd_Box from OCP.BRep import BRep_Tool from OCP.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse from OCP.BRepBndLib import BRepBndLib from OCP.BRepBuilderAPI import ( BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakePolygon, ) from OCP.BRepGProp import BRepGProp from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.BRepPrimAPI import ( BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakePrism, ) from OCP.BinTools import BinTools from OCP.GC import GC_MakeArcOfCircle from OCP.Geom import Geom_BSplineCurve from OCP.GeomAbs import GeomAbs_Cylinder, GeomAbs_Line, GeomAbs_Plane from OCP.GProp import GProp_GProps from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Vec from OCP.TColgp import TColgp_Array1OfPnt from OCP.TColStd import TColStd_Array1OfInteger, TColStd_Array1OfReal from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_SOLID from OCP.TopExp import TopExp from OCP.TopLoc import TopLoc_Location from OCP.TopoDS import TopoDS, TopoDS_Shape from OCP.TopTools import TopTools_IndexedMapOfShape, TopTools_ListOfShape REL = 1e-9 @pytest.fixture(scope="module") def inc1(manifest): if "inc1" not in manifest: pytest.skip("manifest predates the Inc 1 reference block") return manifest["inc1"] # -------------------------------------------------------------------------- # Helpers, deliberately mirroring tools/gen_fixtures.py # -------------------------------------------------------------------------- def sub_shapes(shape, kind=None): smap = TopTools_IndexedMapOfShape() if kind is None: TopExp.MapShapes_s(shape, smap) else: TopExp.MapShapes_s(shape, kind, smap) return [smap.FindKey(i) for i in range(1, smap.Extent() + 1)] def measure(shape) -> dict: vol, area = GProp_GProps(), GProp_GProps() BRepGProp.VolumeProperties_s(shape, vol) BRepGProp.SurfaceProperties_s(shape, area) box = Bnd_Box() BRepBndLib.Add_s(shape, box) return { "volume": vol.Mass(), "area": area.Mass(), "bbox": None if box.IsVoid() else list(box.Get()), "n_faces": len(sub_shapes(shape, TopAbs_FACE)), "n_edges": len(sub_shapes(shape, TopAbs_EDGE)), "n_solids": len(sub_shapes(shape, TopAbs_SOLID)), } def history(builder, pre, post) -> list[dict]: all_post = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(post, all_post) return [ { "pre": i, "deleted": bool(builder.IsDeleted(face)), "modified": sorted( all_post.FindIndex(s) - 1 for s in builder.Modified(face) ), "generated": sorted( all_post.FindIndex(s) - 1 for s in builder.Generated(face) ), } for i, face in enumerate(sub_shapes(pre, TopAbs_FACE)) ] def brep_sha(shape) -> str: buf = io.BytesIO() BinTools.Write_s(shape, buf) return hashlib.sha256(buf.getvalue()).hexdigest() def linear(shape) -> float: props = GProp_GProps() BRepGProp.LinearProperties_s(shape, props) return props.Mass() def assert_close(actual, expected, what: str) -> None: if expected is None: assert actual is None, what return if isinstance(expected, list): assert actual == pytest.approx(expected, rel=REL), what else: assert actual == pytest.approx(expected, rel=REL), what # -------------------------------------------------------------------------- # Measurement parity over the committed fixtures # -------------------------------------------------------------------------- def test_measurements_match_stock(inc1, fixture_shapes): for name, expected in inc1["measure"].items(): got = measure(fixture_shapes[name]) for key in ("n_faces", "n_edges", "n_solids"): assert got[key] == expected[key], f"{name}.{key}" for key in ("volume", "area", "bbox"): assert_close(got[key], expected[key], f"{name}.{key}") def test_face_props_match_stock(inc1, fixture_shapes): """Per-face area and centroid, in MapShapes(FACE) order. Also the reference n3xd_ocp.measure.face_surface_props has to reproduce: the anchor path keys on this ordering. """ for name, expected in inc1["face_props"].items(): faces = sub_shapes(fixture_shapes[name], TopAbs_FACE) assert len(faces) == len(expected), f"{name}: face count" for i, (face, row) in enumerate(zip(faces, expected, strict=True)): props = GProp_GProps() BRepGProp.SurfaceProperties_s(face, props) c = props.CentreOfMass() assert_close( [props.Mass(), c.X(), c.Y(), c.Z()], row, f"{name}.face[{i}]" ) def test_surface_properties_eps_overload_is_bound(): """The 3-argument form exists and converges on the same answer for an analytic face. The app's spline-area accuracy work needs this knob.""" box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() default, eps = GProp_GProps(), GProp_GProps() BRepGProp.SurfaceProperties_s(box, default) BRepGProp.SurfaceProperties_s(box, eps, 1e-6) assert eps.Mass() == pytest.approx(default.Mass(), rel=1e-9) # -------------------------------------------------------------------------- # Boolean history — the topological-naming substrate # -------------------------------------------------------------------------- def _cut_cylinder_op(): op = BRepAlgoAPI_Cut() base = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() tool = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(5, 10, 0), gp_Dir(0, 0, 1)), 3.0, 30.0 ).Shape() args, tools = TopTools_ListOfShape(), TopTools_ListOfShape() args.Append(base) tools.Append(tool) op.SetArguments(args) op.SetTools(tools) op.Build() return op, base def test_boolean_history_matches_stock_exactly(inc1): op, base = _cut_cylinder_op() assert op.IsDone() assert history(op, base, op.Shape()) == inc1["cut_cylinder_history"] def test_history_of_an_untracked_subshape_raises_catchably(): """The app wraps every history call in `except Exception`, because a builder need not track an arbitrary sub-shape. Whatever OCCT does there must reach Python as an exception, never as an abort.""" op, _base = _cut_cylinder_op() stranger = BRepPrimAPI_MakeBox(1.0, 1.0, 1.0).Shape() try: op.Modified(stranger) op.Generated(stranger) op.IsDeleted(stranger) except Exception: # noqa: BLE001 — the point is that it is catchable pass def test_executing_boolean_constructors_are_not_bound(): """The two-argument constructors execute during construction; the app's call sites also called Build(), so the operation ran twice and the first, destructive pass could mutate shared inputs. Refusing to bind them is what makes that unrepresentable.""" a = BRepPrimAPI_MakeBox(10.0, 10.0, 10.0).Shape() b = BRepPrimAPI_MakeBox(5.0, 5.0, 5.0).Shape() for cls in (BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse): with pytest.raises(TypeError): cls(a, b) # -------------------------------------------------------------------------- # Constructors # -------------------------------------------------------------------------- def test_arc_edge_matches_stock(inc1): arc = GC_MakeArcOfCircle( gp_Pnt(0, 0, 0), gp_Pnt(5, 5, 0), gp_Pnt(10, 0, 0) ).Value() edge = BRepBuilderAPI_MakeEdge(arc).Edge() expected = inc1["constructions"]["arc_edge"] assert brep_sha(edge) == expected["sha256"] assert_close(linear(edge), expected["length"], "arc length") def test_bspline_edge_matches_stock(inc1): """A Python-constructed transient handed straight to the kernel. This is the path that makes nb::init<> unusable for transients: the edge builder stores a handle to the curve, so the curve must own its own heap storage rather than live inside the Python instance. """ poles = TColgp_Array1OfPnt(1, 4) for i, (x, y) in enumerate([(0, 0), (3, 6), (7, -4), (10, 2)], start=1): poles.SetValue(i, gp_Pnt(float(x), float(y), 0.0)) knots = TColStd_Array1OfReal(1, 2) knots.SetValue(1, 0.0) knots.SetValue(2, 1.0) mults = TColStd_Array1OfInteger(1, 2) mults.SetValue(1, 4) mults.SetValue(2, 4) curve = Geom_BSplineCurve(poles, knots, mults, 3) edge = BRepBuilderAPI_MakeEdge(curve).Edge() del curve, poles, knots, mults # the edge must not depend on them expected = inc1["constructions"]["spline_edge"] assert brep_sha(edge) == expected["sha256"] assert_close(linear(edge), expected["length"], "spline length") def test_prism_matches_stock_including_history(inc1): wire = BRepBuilderAPI_MakePolygon( gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0), gp_Pnt(10, 6, 0), gp_Pnt(0, 6, 0), True ).Wire() face = BRepBuilderAPI_MakeFace(wire, True).Face() maker = BRepPrimAPI_MakePrism(face, gp_Vec(0.0, 0.0, 4.0)) prism = maker.Shape() expected = inc1["constructions"]["prism"] assert brep_sha(prism) == expected["sha256"] got = measure(prism) for key in ("n_faces", "n_edges", "n_solids"): assert got[key] == expected[key], key for key in ("volume", "area", "bbox"): assert_close(got[key], expected[key], key) # An extrude's provenance is read off exactly this map. assert history(maker, face, prism) == expected["history"] def test_meshing_matches_stock_counts(inc1): shape = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() BRepMesh_IncrementalMesh(shape, 0.1, False, 0.5, True) got = [] for i, face in enumerate(sub_shapes(shape, TopAbs_FACE)): loc = TopLoc_Location() tri = BRep_Tool.Triangulation_s(TopoDS.Face_s(face), loc) if tri is None: continue # id gaps are preserved, exactly as tessellation.py does got.append([i, tri.NbNodes(), tri.NbTriangles()]) assert got == inc1["box_meshed_counts"] # -------------------------------------------------------------------------- # Adaptors — the app's most-used query path # -------------------------------------------------------------------------- def test_adaptor_reports_surface_kind_and_quadric(): """`BRepAdaptor_Surface(f).Cylinder().Radius()` is the shape of nearly every reference-inference call in the app, and it crosses three modules: the virtuals live on Adaptor3d_Surface, the enum in GeomAbs, the result in gp. """ cyl = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), 4.0, 12.0 ).Shape() kinds = {} for face in sub_shapes(cyl, TopAbs_FACE): adaptor = BRepAdaptor_Surface(TopoDS.Face_s(face)) kinds.setdefault(adaptor.GetType(), []).append(adaptor) assert len(kinds[GeomAbs_Plane]) == 2 lateral = kinds[GeomAbs_Cylinder][0] assert lateral.Cylinder().Radius() == pytest.approx(4.0) assert lateral.Cylinder().Axis().Direction().Z() == pytest.approx(1.0) def test_curve_adaptor_reports_line_and_bounds(): box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() edge = TopoDS.Edge_s(sub_shapes(box, TopAbs_EDGE)[0]) adaptor = BRepAdaptor_Curve(edge) assert adaptor.GetType() == GeomAbs_Line assert adaptor.LastParameter() > adaptor.FirstParameter() direction = adaptor.Line().Direction() assert abs(direction.X()) + abs(direction.Y()) + abs(direction.Z()) == ( pytest.approx(1.0) ) def test_surface_out_parameter_derivatives(): """D1 keeps its out-parameters: the app pre-allocates and reads back.""" box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() face = TopoDS.Face_s(sub_shapes(box, TopAbs_FACE)[0]) adaptor = BRepAdaptor_Surface(face) point, du, dv = gp_Pnt(), gp_Vec(), gp_Vec() adaptor.D1(0.5, 0.5, point, du, dv) assert du.Crossed(dv).Magnitude() > 0.0 def test_brep_tool_surface_round_trips_through_geom(): """Surface_s hands back a live Geom_Surface, which must survive the trip back into the kernel — the projection is how feature recognition measures tangency.""" from OCP.GeomAPI import GeomAPI_ProjectPointOnSurf box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() face = TopoDS.Face_s(sub_shapes(box, TopAbs_FACE)[0]) surf = BRep_Tool.Surface_s(face) proj = GeomAPI_ProjectPointOnSurf(gp_Pnt(-5.0, 10.0, 15.0), surf) assert proj.IsDone() assert proj.NbPoints() >= 1 u, v = proj.LowerDistanceParameters() assert isinstance(u, float) and isinstance(v, float) def test_bnd_box_get_returns_a_tuple(): box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() bbox = Bnd_Box() BRepBndLib.Add_s(box, bbox) xmin, ymin, zmin, xmax, ymax, zmax = bbox.Get() assert xmax - xmin == pytest.approx(10.0, abs=1e-6) assert ymax - ymin == pytest.approx(20.0, abs=1e-6) assert zmax - zmin == pytest.approx(30.0, abs=1e-6) def test_history_survives_its_builder(): """Sub-shapes come back by value, so a history list outlives the operator that produced it — the app keeps provenance around long after.""" op, base = _cut_cylinder_op() pierced = sub_shapes(base, TopAbs_FACE)[4] modified = list(op.Modified(pierced)) del op assert modified assert all(isinstance(s, TopoDS_Shape) and not s.IsNull() for s in modified)