10C Inc 2: feature tail
119 of the app's 138 symbols now resolve; only the I/O modules and the Inc 4 tail are left. New: GCE2d, GCPnts, BRepFilletAPI, BRepOffsetAPI, ShapeAnalysis, ShapeFix, ShapeUpgrade, BRepCheck, BRepTools, BRepLib, BRepExtrema, BRepClass3d, IntCurvesFace, plus TopTools_HSequenceOfShape. The fillet and chamfer builders derive BRepBuilderAPI_MakeShape, so their history comes from the base bound in Inc 1 — which is what lets the app read a blend's provenance the same way it reads a boolean's. The Inc 2 gate compares that history exactly, alongside the splitter's, which is what sketch-region attribution depends on. Two shapes of deviation, both commented where they are bound: - Trailing enum arguments are left off four constructors (fillet's ChFi3d_FilletShape, MakeThickSolidByJoin's mode/join pair, BRepExtrema's Extrema flags, and Inc 1's GeomAPI_ProjectPointOnSurf). An unregistered enum cannot serve as a default argument — nanobind converts defaults at binding time, so it fails the whole module's import with std::bad_cast. The app never overrides any of them, so OCCT's own defaults apply and behaviour is unchanged. - ShapeAnalysis_FreeBounds.ConnectEdgesToWires reassigns a Handle& out parameter, which the caster cannot honour because it hands C++ a copy of the handle. The lambda splices the result into the sequence the caller passed, so the pass-empty-then-read shape the app uses still works. This is the one place upstream's holder semantics and ours differ observably. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
This commit is contained in:
@@ -499,5 +499,197 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"inc2": {
|
||||
"fillet": {
|
||||
"sha256": "4da9eb256a1e012b94655f6ecd02b34590c4e058beb336794fb68c3047c0070c",
|
||||
"volume": 5974.247779607694,
|
||||
"area": 2172.530964914873,
|
||||
"bbox": [
|
||||
-1.000000004440892e-07,
|
||||
-1.000000004440892e-07,
|
||||
-1e-07,
|
||||
10.0000001,
|
||||
20.0000001,
|
||||
30.0000001
|
||||
],
|
||||
"n_faces": 7,
|
||||
"n_edges": 15,
|
||||
"n_solids": 1,
|
||||
"history": [
|
||||
{
|
||||
"pre": 0,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
3
|
||||
],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 1,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 2,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
37
|
||||
],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 3,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 4,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
32
|
||||
],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 5,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
13
|
||||
],
|
||||
"generated": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"chamfer": {
|
||||
"sha256": "cdf8d040a239e6202f94e38fc0fc9595e475180c7067a5f1e2391d9ed95d03af",
|
||||
"volume": 5966.25,
|
||||
"area": 2171.389610306789,
|
||||
"bbox": [
|
||||
-1.000000001110223e-07,
|
||||
-1.000000001110223e-07,
|
||||
-1e-07,
|
||||
10.0000001,
|
||||
20.0000001,
|
||||
30.000000100000005
|
||||
],
|
||||
"n_faces": 7,
|
||||
"n_edges": 15,
|
||||
"n_solids": 1
|
||||
},
|
||||
"splitter": {
|
||||
"volume": 6000.0,
|
||||
"area": 3400.0,
|
||||
"bbox": [
|
||||
-1.5e-07,
|
||||
-1.5e-07,
|
||||
-1.5e-07,
|
||||
10.00000015,
|
||||
20.00000015,
|
||||
30.00000015
|
||||
],
|
||||
"n_faces": 16,
|
||||
"n_edges": 28,
|
||||
"n_solids": 3,
|
||||
"history": [
|
||||
{
|
||||
"pre": 0,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
3,
|
||||
37,
|
||||
61
|
||||
],
|
||||
"generated": [
|
||||
8,
|
||||
41
|
||||
]
|
||||
},
|
||||
{
|
||||
"pre": 1,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
33,
|
||||
57,
|
||||
81
|
||||
],
|
||||
"generated": [
|
||||
24,
|
||||
53
|
||||
]
|
||||
},
|
||||
{
|
||||
"pre": 2,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 3,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 4,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
25,
|
||||
54,
|
||||
78
|
||||
],
|
||||
"generated": [
|
||||
22,
|
||||
51
|
||||
]
|
||||
},
|
||||
{
|
||||
"pre": 5,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
13,
|
||||
44,
|
||||
68
|
||||
],
|
||||
"generated": [
|
||||
15,
|
||||
46
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"unified": {
|
||||
"volume": 12500.0,
|
||||
"area": 3650.0,
|
||||
"bbox": [
|
||||
-1.5e-07,
|
||||
-1.5e-07,
|
||||
-1.5e-07,
|
||||
25.00000015,
|
||||
25.00000015,
|
||||
30.00000015
|
||||
],
|
||||
"n_faces": 12,
|
||||
"n_edges": 30,
|
||||
"n_solids": 1
|
||||
},
|
||||
"distance": {
|
||||
"is_done": true,
|
||||
"value": 30.0,
|
||||
"n_solutions": 4
|
||||
},
|
||||
"classifier": {
|
||||
"inside": 0,
|
||||
"outside": 1
|
||||
},
|
||||
"uv_bounds": [
|
||||
0.0,
|
||||
30.0,
|
||||
-20.0,
|
||||
0.0
|
||||
],
|
||||
"outer_wire_edges": 4
|
||||
}
|
||||
}
|
||||
|
||||
224
tests/test_inc2_features.py
Normal file
224
tests/test_inc2_features.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user