10C Inc 1: core modeling
Binds the modeling core the app builds every feature out of — 93 of its 138 symbols now resolve, up from 34. New modules: GeomAbs, Geom2d, Geom, TCollection, TColgp, TColStd, GProp, BRepGProp, Bnd, BRepBndLib, Adaptor3d, BRepAdaptor, GeomLProp, GeomAPI, BRepBuilderAPI, BOPAlgo, BRepAlgoAPI, BRepPrimAPI, GC, BRepMesh; gp and BRep_Tool completed. Three structural decisions: - BRepBuilderAPI_MakeShape carries Build/Shape/Generated/Modified/IsDeleted for every maker in the binding, so the booleans, the primitives and (later) the fillet builders all answer the app's duck-typed provenance layer through ordinary virtual dispatch. History lists come back copied, so they outlive the builder. - The executing two-argument BRepAlgoAPI constructors stay unbound; operands go in through SetArguments/SetTools. Section keeps Init1/Init2, which are plain setters. BOPAlgo moved up from Inc 2 — SetGlue needs its enum. - Adaptor3d is registered although the app never imports it: every method it calls on BRepAdaptor_Curve/Surface is a virtual declared there, so binding them once on the bases leaves mod_BRepAdaptor.cpp with just constructors. Gate: tests/test_inc1_modeling.py against reference values gen_fixtures.py now records from the stock wheel — measurements, per-face area/centroid in map order, mesh counts, and the boolean history map compared exactly, since that is the substrate the app's topological naming is built on. tools/sigdiff.py compares our bound surface against stock's, because a wrong nb::init<> is silent: MakePrism's five-argument form bound OCCT's semi-infinite gp_Dir overload (gp_Dir converts from gp_Vec), producing a valid solid of the wrong shape with the flags shifted along. The fixture digest caught it; sigdiff finds the class of bug directly, and now reports only one deliberate deviation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
This commit is contained in:
@@ -32,5 +32,472 @@
|
||||
"size": 171,
|
||||
"faces": 0
|
||||
}
|
||||
},
|
||||
"inc1": {
|
||||
"measure": {
|
||||
"box": {
|
||||
"volume": 6000.0,
|
||||
"area": 2200.0,
|
||||
"bbox": [
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
10.0000001,
|
||||
20.0000001,
|
||||
30.0000001
|
||||
],
|
||||
"n_faces": 6,
|
||||
"n_edges": 12,
|
||||
"n_solids": 1
|
||||
},
|
||||
"box_meshed": {
|
||||
"volume": 6000.0,
|
||||
"area": 2200.0,
|
||||
"bbox": [
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
10.0000001,
|
||||
20.0000001,
|
||||
30.0000001
|
||||
],
|
||||
"n_faces": 6,
|
||||
"n_edges": 12,
|
||||
"n_solids": 1
|
||||
},
|
||||
"fused": {
|
||||
"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
|
||||
},
|
||||
"cut_cylinder": {
|
||||
"volume": 5151.769983530756,
|
||||
"area": 2708.9380098815463,
|
||||
"bbox": [
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
10.0000001,
|
||||
20.0000001,
|
||||
30.0000001
|
||||
],
|
||||
"n_faces": 7,
|
||||
"n_edges": 15,
|
||||
"n_solids": 1
|
||||
},
|
||||
"located_compound": {
|
||||
"volume": 12000.0,
|
||||
"area": 4400.0,
|
||||
"bbox": [
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
-1e-07,
|
||||
60.0000001,
|
||||
20.0000001,
|
||||
30.0000001
|
||||
],
|
||||
"n_faces": 12,
|
||||
"n_edges": 24,
|
||||
"n_solids": 2
|
||||
},
|
||||
"empty_compound": {
|
||||
"volume": 0.0,
|
||||
"area": 0.0,
|
||||
"bbox": null,
|
||||
"n_faces": 0,
|
||||
"n_edges": 0,
|
||||
"n_solids": 0
|
||||
}
|
||||
},
|
||||
"face_props": {
|
||||
"box": [
|
||||
[
|
||||
600.0,
|
||||
0.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
600.0,
|
||||
10.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
20.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
30.0
|
||||
]
|
||||
],
|
||||
"box_meshed": [
|
||||
[
|
||||
600.0,
|
||||
0.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
600.0,
|
||||
10.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
20.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
30.0
|
||||
]
|
||||
],
|
||||
"fused": [
|
||||
[
|
||||
600.0,
|
||||
0.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
30.0
|
||||
],
|
||||
[
|
||||
200.0,
|
||||
3.7500000000000004,
|
||||
20.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
10.0,
|
||||
7.5,
|
||||
14.999999999999996
|
||||
],
|
||||
[
|
||||
324.99999999999994,
|
||||
16.73076923076923,
|
||||
15.57692307692308,
|
||||
5.0
|
||||
],
|
||||
[
|
||||
99.99999999999999,
|
||||
5.0,
|
||||
22.5,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
324.99999999999994,
|
||||
16.73076923076923,
|
||||
15.57692307692308,
|
||||
25.0
|
||||
],
|
||||
[
|
||||
299.99999999999994,
|
||||
17.5,
|
||||
5.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
399.99999999999994,
|
||||
15.0,
|
||||
25.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
399.99999999999994,
|
||||
25.0,
|
||||
15.0,
|
||||
15.0
|
||||
]
|
||||
],
|
||||
"cut_cylinder": [
|
||||
[
|
||||
600.0,
|
||||
0.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
171.72566611769184,
|
||||
5.0,
|
||||
10.0,
|
||||
30.000000000000004
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
20.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
171.72566611769184,
|
||||
5.0,
|
||||
10.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
600.0,
|
||||
10.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
565.4866776461628,
|
||||
5.0,
|
||||
10.0,
|
||||
15.000000000000002
|
||||
]
|
||||
],
|
||||
"located_compound": [
|
||||
[
|
||||
600.0,
|
||||
0.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
600.0,
|
||||
10.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
5.0,
|
||||
20.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
5.0,
|
||||
10.0,
|
||||
30.0
|
||||
],
|
||||
[
|
||||
600.0,
|
||||
50.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
600.0,
|
||||
60.0,
|
||||
10.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
55.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
300.0,
|
||||
55.0,
|
||||
20.0,
|
||||
15.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
55.0,
|
||||
10.0,
|
||||
0.0
|
||||
],
|
||||
[
|
||||
199.99999999999997,
|
||||
55.0,
|
||||
10.0,
|
||||
30.0
|
||||
]
|
||||
]
|
||||
},
|
||||
"cut_cylinder_history": [
|
||||
{
|
||||
"pre": 0,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 1,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 2,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 3,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 4,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
33
|
||||
],
|
||||
"generated": []
|
||||
},
|
||||
{
|
||||
"pre": 5,
|
||||
"deleted": false,
|
||||
"modified": [
|
||||
20
|
||||
],
|
||||
"generated": []
|
||||
}
|
||||
],
|
||||
"box_meshed_counts": [
|
||||
[
|
||||
0,
|
||||
4,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
4,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
4,
|
||||
2
|
||||
],
|
||||
[
|
||||
3,
|
||||
4,
|
||||
2
|
||||
],
|
||||
[
|
||||
4,
|
||||
4,
|
||||
2
|
||||
],
|
||||
[
|
||||
5,
|
||||
4,
|
||||
2
|
||||
]
|
||||
],
|
||||
"constructions": {
|
||||
"arc_edge": {
|
||||
"sha256": "2b6eadbe678c8e13d04cfa1fdf6d6d475776d168639dbb38b90452c9f1481535",
|
||||
"length": 15.707963267948966
|
||||
},
|
||||
"spline_edge": {
|
||||
"sha256": "71a9b3c8aee46b338728fc03b98931d4087c5df5962e9745d70fcfd3cc1a3133",
|
||||
"length": 12.236066959998437
|
||||
},
|
||||
"prism": {
|
||||
"sha256": "0730ad0a74dda44760b4a43b4b7f22ed58c672d8aeb8f3fd483969b92cbf3d2f",
|
||||
"volume": 239.99999999999997,
|
||||
"area": 247.99999999999994,
|
||||
"bbox": [
|
||||
-1.0000000000000002e-07,
|
||||
-1.0000000000000002e-07,
|
||||
-1.0000000000000002e-07,
|
||||
10.0000001,
|
||||
6.0000001,
|
||||
4.0000001
|
||||
],
|
||||
"n_faces": 6,
|
||||
"n_edges": 12,
|
||||
"n_solids": 1,
|
||||
"history": [
|
||||
{
|
||||
"pre": 0,
|
||||
"deleted": false,
|
||||
"modified": [],
|
||||
"generated": [
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
386
tests/test_inc1_modeling.py
Normal file
386
tests/test_inc1_modeling.py
Normal file
@@ -0,0 +1,386 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user