"""Inc 2 gate: the feature tail reproduces the stock wheel's answers. Same contract as test_inc1_modeling.py — see its module docstring for why the app's own suite cannot gate an increment and what the tolerances mean. The fillet history is the load-bearing check here: a blend's provenance is read exactly like a boolean's, and it is what attributes the new faces back to the feature that made them. """ from __future__ import annotations import pytest from OCP.BRepAlgoAPI import BRepAlgoAPI_Splitter from OCP.BRepClass3d import BRepClass3d_SolidClassifier from OCP.BRepExtrema import BRepExtrema_DistShapeShape from OCP.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox from OCP.BRepTools import BRepTools, BRepTools_WireExplorer from OCP.ShapeUpgrade import ShapeUpgrade_UnifySameDomain from OCP.TopAbs import TopAbs_EDGE, TopAbs_FACE from OCP.TopoDS import TopoDS from OCP.TopTools import TopTools_ListOfShape from .test_inc1_modeling import assert_close, brep_sha, history, measure, sub_shapes @pytest.fixture(scope="module") def inc2(manifest): if "inc2" not in manifest: pytest.skip("manifest predates the Inc 2 reference block") return manifest["inc2"] def _box(): return BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() def _compare(got: dict, expected: dict, what: str) -> None: for key in ("n_faces", "n_edges", "n_solids"): assert got[key] == expected[key], f"{what}.{key}" for key in ("volume", "area", "bbox"): assert_close(got[key], expected[key], f"{what}.{key}") def test_fillet_matches_stock_including_history(inc2): box = _box() maker = BRepFilletAPI_MakeFillet(box) maker.Add(2.0, TopoDS.Edge_s(sub_shapes(box, TopAbs_EDGE)[0])) maker.Build() assert maker.IsDone() filleted = maker.Shape() expected = inc2["fillet"] assert brep_sha(filleted) == expected["sha256"] _compare(measure(filleted), expected, "fillet") # The blend's provenance, read the same way a boolean's is. assert history(maker, box, filleted) == expected["history"] def test_chamfer_matches_stock(inc2): box = _box() maker = BRepFilletAPI_MakeChamfer(box) maker.Add(1.5, TopoDS.Edge_s(sub_shapes(box, TopAbs_EDGE)[0])) maker.Build() assert maker.IsDone() expected = inc2["chamfer"] assert brep_sha(maker.Shape()) == expected["sha256"] _compare(measure(maker.Shape()), expected, "chamfer") def test_splitter_matches_stock_including_history(inc2): from OCP.gp import gp_Pnt box = _box() splitter = BRepAlgoAPI_Splitter() args, tools = TopTools_ListOfShape(), TopTools_ListOfShape() args.Append(box) tools.Append(BRepPrimAPI_MakeBox(gp_Pnt(-5, 5, -5), 20.0, 1.0, 40.0).Shape()) splitter.SetArguments(args) splitter.SetTools(tools) splitter.Build() assert splitter.IsDone() expected = inc2["splitter"] _compare(measure(splitter.Shape()), expected, "splitter") # Region attribution in the sketch builder reads exactly this. assert history(splitter, box, splitter.Shape()) == expected["history"] def test_unify_same_domain_matches_stock(inc2, fixture_shapes): unify = ShapeUpgrade_UnifySameDomain(fixture_shapes["fused"], True, True, True) unify.Build() _compare(measure(unify.Shape()), inc2["unified"], "unified") def test_distance_and_classifier_match_stock(inc2): from OCP.gp import gp_Pnt dist = BRepExtrema_DistShapeShape( _box(), BRepPrimAPI_MakeBox(gp_Pnt(40, 0, 0), 5.0, 5.0, 5.0).Shape() ) assert dist.IsDone() == inc2["distance"]["is_done"] assert dist.NbSolution() == inc2["distance"]["n_solutions"] assert_close(dist.Value(), inc2["distance"]["value"], "distance") box = _box() inside = BRepClass3d_SolidClassifier(box, gp_Pnt(5, 10, 15), 1e-7) outside = BRepClass3d_SolidClassifier(box, gp_Pnt(50, 10, 15), 1e-7) assert int(inside.State()) == inc2["classifier"]["inside"] assert int(outside.State()) == inc2["classifier"]["outside"] def test_brep_tools_queries_match_stock(inc2): face = TopoDS.Face_s(sub_shapes(_box(), TopAbs_FACE)[0]) assert_close(list(BRepTools.UVBounds_s(face)), inc2["uv_bounds"], "uv_bounds") outer = BRepTools.OuterWire_s(face) assert len(sub_shapes(outer, TopAbs_EDGE)) == inc2["outer_wire_edges"] def test_wire_explorer_walks_in_connection_order(): """WireExplorer follows connectivity, which is the whole reason to use it over the map order MapShapes gives.""" face = TopoDS.Face_s(sub_shapes(_box(), TopAbs_FACE)[0]) explorer = BRepTools_WireExplorer(BRepTools.OuterWire_s(face)) walked = [] while explorer.More(): walked.append(explorer.Current()) explorer.Next() assert len(walked) == 4 # Results outlive the explorer: sub-shapes come back by value. del explorer assert all(not e.IsNull() for e in walked) def test_clean_drops_the_existing_triangulation(): """OCCT keeps whatever mesh a shape already carries, so re-meshing at a different deflection is a no-op without this — which is why the tessellation path calls it before its second pass.""" from OCP.BRep import BRep_Tool from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.TopLoc import TopLoc_Location shape = _box() BRepMesh_IncrementalMesh(shape, 0.1, False, 0.5, True) face = TopoDS.Face_s(sub_shapes(shape, TopAbs_FACE)[0]) assert BRep_Tool.Triangulation_s(face, TopLoc_Location()) is not None BRepTools.Clean_s(shape) face = TopoDS.Face_s(sub_shapes(shape, TopAbs_FACE)[0]) assert BRep_Tool.Triangulation_s(face, TopLoc_Location()) is None def test_free_bounds_reports_an_open_shell(): """A watertight solid has no free boundary; a shell missing one face exposes that face's four edges. This is how import health tells the two apart, and it exercises Sewing on the way. Note it wants a genuinely sewn shell: a bare face reports nothing, in this binding and in the stock wheel alike. """ from OCP.BRepBuilderAPI import BRepBuilderAPI_Sewing from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds def free_edges(shape) -> int: bounds = ShapeAnalysis_FreeBounds(shape, 1e-6) return sum( len(sub_shapes(wires, TopAbs_EDGE)) for wires in (bounds.GetClosedWires(), bounds.GetOpenWires()) if not wires.IsNull() ) assert free_edges(_box()) == 0 box = _box() sewing = BRepBuilderAPI_Sewing(1e-6) for face in sub_shapes(box, TopAbs_FACE)[:-1]: # leave one face off sewing.Add(face) sewing.Perform() assert free_edges(sewing.SewedShape()) == 4 def test_ray_intersector_finds_the_opposite_wall(): """The surface-pattern feature measures wall thickness this way.""" from OCP.gp import gp_Dir, gp_Lin, gp_Pnt from OCP.IntCurvesFace import IntCurvesFace_ShapeIntersector inter = IntCurvesFace_ShapeIntersector() inter.Load(_box(), 1e-7) inter.PerformNearest( gp_Lin(gp_Pnt(-5.0, 10.0, 15.0), gp_Dir(1.0, 0.0, 0.0)), 0.0, 1.0e6 ) assert inter.IsDone() assert inter.NbPnt() > 0 assert inter.WParameter(1) == pytest.approx(5.0, abs=1e-6) def test_curve_length_matches_the_edge(): from OCP.BRepAdaptor import BRepAdaptor_Curve from OCP.GCPnts import GCPnts_AbscissaPoint, GCPnts_TangentialDeflection edge = TopoDS.Edge_s(sub_shapes(_box(), TopAbs_EDGE)[0]) adaptor = BRepAdaptor_Curve(edge) length = GCPnts_AbscissaPoint.Length_s(adaptor) assert length in (pytest.approx(10.0), pytest.approx(20.0), pytest.approx(30.0)) # The discretiser the viewport's edge polylines come from. points = GCPnts_TangentialDeflection(adaptor, 0.15, 0.05) assert points.NbPoints() >= 2 assert not points.Value(1).IsEqual(points.Value(points.NbPoints()), 1e-9) def test_thick_solid_by_simple_thickens_a_face(): """The shape rib and the open-profile extrude both lean on this.""" from OCP.BRepOffsetAPI import BRepOffsetAPI_MakeThickSolid face = TopoDS.Face_s(sub_shapes(_box(), TopAbs_FACE)[0]) maker = BRepOffsetAPI_MakeThickSolid() maker.MakeThickSolidBySimple(face, 2.0) maker.Build() assert maker.IsDone() assert measure(maker.Shape())["n_solids"] == 1