"""Generate the .brep byte-identity fixtures. Runs under the STOCK cadquery-ocp-novtk wheel, not this binding — the fixtures are the reference the Inc 0 gate compares against. From the app checkout: app/.venv/bin/python ../ocp/tools/gen_fixtures.py Two properties are recorded, and the first is checked here rather than assumed: * stock is idempotent — reading a shape and writing it back reproduces the same bytes. Without that the gate would be comparing against a moving target, and a mismatch would say nothing. * the digest of those bytes, which is what the gate reproduces. This matters beyond IPC: cad/derive.py content-addresses BREP payloads as payloads/derived/brep/.brep and stores the ref in the document, so a binding that serialises differently rewrites every derived payload. Face counts are recorded alongside, pinning the map ordering that face and edge identity depend on throughout the topology code. From Inc 1 on, the manifest also carries a per-increment reference block, since the app's own tests cannot gate an increment: backend/tests/conftest.py imports n3xd.main, so every one of them fails at collection until the last module is bound. What the blocks record is chosen to be robust: counts and boolean history maps are topological and compare exactly, measurements compare at a relative tolerance. See docs/adding-symbols.md. """ from __future__ import annotations import hashlib import io import json import pathlib import sys from OCP.BinTools import BinTools from OCP.BRep import BRep_Builder from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder from OCP.gp import gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec from OCP.TopAbs import TopAbs_FACE from OCP.TopExp import TopExp from OCP.TopLoc import TopLoc_Location from OCP.TopoDS import TopoDS_Compound, TopoDS_Shape from OCP.TopTools import TopTools_IndexedMapOfShape OUT = pathlib.Path(__file__).resolve().parent.parent / "tests" / "data" def _box(dx=10.0, dy=20.0, dz=30.0) -> TopoDS_Shape: return BRepPrimAPI_MakeBox(dx, dy, dz).Shape() def _meshed_box() -> TopoDS_Shape: shape = _box() BRepMesh_IncrementalMesh(shape, 0.1, False, 0.5, True) return shape def _fused() -> TopoDS_Shape: op = BRepAlgoAPI_Fuse() from OCP.TopTools import TopTools_ListOfShape args, tools = TopTools_ListOfShape(), TopTools_ListOfShape() args.Append(_box()) tools.Append( BRepPrimAPI_MakeBox(gp_Pnt(5, 5, 5), 20.0, 20.0, 20.0).Shape() ) op.SetArguments(args) op.SetTools(tools) op.Build() return op.Shape() def _cut_cylinder_op(): """The deferred cut, returned unbuilt operands and all, so the Inc 1 block can read its history against the same operand shapes.""" from OCP.TopTools import TopTools_ListOfShape op = BRepAlgoAPI_Cut() base = _box() 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, tool def _cut_cylinder() -> TopoDS_Shape: op, _base, _tool = _cut_cylinder_op() return op.Shape() def _located_compound() -> TopoDS_Shape: """Exercises the location/TShape sharing part of the format.""" builder = BRep_Builder() comp = TopoDS_Compound() builder.MakeCompound(comp) base = _box() builder.Add(comp, base) trsf = gp_Trsf() trsf.SetTranslation(gp_Vec(50.0, 0.0, 0.0)) builder.Add(comp, base.Moved(TopLoc_Location(trsf))) return comp def _empty_compound() -> TopoDS_Shape: builder = BRep_Builder() comp = TopoDS_Compound() builder.MakeCompound(comp) return comp SHAPES = { "box": _box, "box_meshed": _meshed_box, "fused": _fused, "cut_cylinder": _cut_cylinder, "located_compound": _located_compound, "empty_compound": _empty_compound, } def write_bytes(shape: TopoDS_Shape) -> bytes: buf = io.BytesIO() BinTools.Write_s(shape, buf) return buf.getvalue() def read_shape(data: bytes) -> TopoDS_Shape: shape = TopoDS_Shape() BinTools.Read_s(shape, io.BytesIO(data)) return shape def face_count(shape: TopoDS_Shape) -> int: faces = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(shape, TopAbs_FACE, faces) return faces.Extent() def _sub_shapes(shape: TopoDS_Shape, kind): smap = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(shape, kind, smap) return [smap.FindKey(i) for i in range(1, smap.Extent() + 1)] def _measure(shape: TopoDS_Shape) -> dict: """Volume, area and bbox, all through the default (non-eps) quadrature.""" from OCP.Bnd import Bnd_Box from OCP.BRepBndLib import BRepBndLib from OCP.BRepGProp import BRepGProp from OCP.GProp import GProp_GProps from OCP.TopAbs import TopAbs_EDGE, TopAbs_SOLID vol, area = GProp_GProps(), GProp_GProps() BRepGProp.VolumeProperties_s(shape, vol) BRepGProp.SurfaceProperties_s(shape, area) box = Bnd_Box() BRepBndLib.Add_s(shape, box) bbox = None if box.IsVoid() else list(box.Get()) return { "volume": vol.Mass(), "area": area.Mass(), "bbox": bbox, "n_faces": face_count(shape), "n_edges": len(_sub_shapes(shape, TopAbs_EDGE)), "n_solids": len(_sub_shapes(shape, TopAbs_SOLID)), } def _face_props(shape: TopoDS_Shape) -> list[list[float]]: """Per-face [area, cx, cy, cz] in MapShapes(FACE) order. This is the reference for n3xd_ocp.measure.face_surface_props, and the ordering contract the whole anchor path depends on. """ from OCP.BRepGProp import BRepGProp from OCP.GProp import GProp_GProps out = [] for face in _sub_shapes(shape, TopAbs_FACE): props = GProp_GProps() BRepGProp.SurfaceProperties_s(face, props) c = props.CentreOfMass() out.append([props.Mass(), c.X(), c.Y(), c.Z()]) return out def _history(builder, pre: TopoDS_Shape, post: TopoDS_Shape) -> list[dict]: """The Modified/Generated/IsDeleted map, keyed by pre-shape face index. Recorded as *post-shape sub-shape indices* rather than shapes, so the comparison is purely topological — this is the substrate the app's topological naming is built on, and it must not drift by a single entry. Indices come from the untyped MapShapes, because a builder may generate a shape of any type (MakePrism answers a face with the solid it swept). """ all_post = TopTools_IndexedMapOfShape() TopExp.MapShapes_s(post, all_post) def index_of(shape: TopoDS_Shape) -> int: return all_post.FindIndex(shape) - 1 # -1 when absent, else 0-based rows = [] for i, face in enumerate(_sub_shapes(pre, TopAbs_FACE)): rows.append( { "pre": i, "deleted": bool(builder.IsDeleted(face)), "modified": sorted(index_of(s) for s in builder.Modified(face)), "generated": sorted(index_of(s) for s in builder.Generated(face)), } ) return rows def _mesh_counts(shape: TopoDS_Shape) -> list[list[int]]: """Per-face [nb_nodes, nb_triangles], skipping faces with no triangulation. Mirrors what cad/tessellation.py extracts, including the "faces without a mesh leave a gap in the id sequence" rule. """ from OCP.BRep import BRep_Tool from OCP.TopoDS import TopoDS out = [] 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 out.append([i, tri.NbNodes(), tri.NbTriangles()]) return out def _constructions() -> dict: """Small shapes built through the Inc 1 constructors the app uses. Recorded by BREP digest plus a measurement, so the test can rebuild them and compare both the bytes and the geometry. """ from OCP.BRepBuilderAPI import ( BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakePolygon, ) from OCP.BRepGProp import BRepGProp from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism from OCP.GC import GC_MakeArcOfCircle from OCP.Geom import Geom_BSplineCurve from OCP.GProp import GProp_GProps from OCP.TColgp import TColgp_Array1OfPnt from OCP.TColStd import TColStd_Array1OfInteger, TColStd_Array1OfReal def length(shape: TopoDS_Shape) -> float: props = GProp_GProps() BRepGProp.LinearProperties_s(shape, props) return props.Mass() arc = GC_MakeArcOfCircle( gp_Pnt(0, 0, 0), gp_Pnt(5, 5, 0), gp_Pnt(10, 0, 0) ).Value() arc_edge = BRepBuilderAPI_MakeEdge(arc).Edge() # A clamped cubic through four poles — the shape sketch_builder/edges.py # builds for a spline element. 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) spline_edge = BRepBuilderAPI_MakeEdge( Geom_BSplineCurve(poles, knots, mults, 3) ).Edge() poly = BRepBuilderAPI_MakePolygon( gp_Pnt(0, 0, 0), gp_Pnt(10, 0, 0), gp_Pnt(10, 6, 0), gp_Pnt(0, 6, 0), True ).Wire() prism_face = BRepBuilderAPI_MakeFace(poly, True).Face() prism_maker = BRepPrimAPI_MakePrism(prism_face, gp_Vec(0.0, 0.0, 4.0)) prism = prism_maker.Shape() return { "arc_edge": { "sha256": hashlib.sha256(write_bytes(arc_edge)).hexdigest(), "length": length(arc_edge), }, "spline_edge": { "sha256": hashlib.sha256(write_bytes(spline_edge)).hexdigest(), "length": length(spline_edge), }, "prism": { "sha256": hashlib.sha256(write_bytes(prism)).hexdigest(), **_measure(prism), # The prism's history is what an extrude's provenance reads. "history": _history(prism_maker, prism_face, prism), }, } def _inc1_reference() -> dict: op, base, _tool = _cut_cylinder_op() return { "measure": {name: _measure(build()) for name, build in SHAPES.items()}, "face_props": { name: _face_props(build()) for name, build in SHAPES.items() if name != "empty_compound" }, "cut_cylinder_history": _history(op, base, op.Shape()), "box_meshed_counts": _mesh_counts(_meshed_box()), "constructions": _constructions(), } def main() -> int: import OCP OUT.mkdir(parents=True, exist_ok=True) manifest: dict = { "generated_by": "cadquery-ocp-novtk", "occt_version": getattr(OCP, "__version__", "unknown"), "shapes": {}, } for name, build in SHAPES.items(): shape = build() data = write_bytes(shape) # The gate is "our rewrite == stock rewrite". If stock itself is not # idempotent for a fixture, that fixture cannot serve as a reference. rewritten = write_bytes(read_shape(data)) if rewritten != data: print(f"FAIL {name}: stock is not idempotent", file=sys.stderr) return 1 (OUT / f"{name}.brep").write_bytes(data) manifest["shapes"][name] = { "sha256": hashlib.sha256(data).hexdigest(), "size": len(data), "faces": face_count(shape), } print(f"{name}: {len(data)} bytes, {manifest['shapes'][name]['faces']} faces") manifest["inc1"] = _inc1_reference() (OUT / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") print(f"\nwrote {len(SHAPES)} fixtures + manifest to {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())