Migrate export_step / export_stl / export_brep off FreeCAD, onto build123d + OCP #23
Labels
No labels
blocked:needs-decision
blocked:preflight
bug
in-progress
in-progress
lint-cleanup
needs-decompose
quickfix
triage-approved
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Mark/pc2cad#23
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Migrate
export_step/export_stl/export_brepoff FreeCAD, onto build123d + OCPArchitectural stance (decided 2026-06-23)
FreeCAD is dropped from
pc2cadexports, no fallback. No_HAS_FREECADconditional block, no migration shim, no "use FreeCAD if installed." Every
export path runs on
OCP+build123d(+numpy-stlfor binary STL), in aplain Python venv, with no system FreeCAD, no conda env, no AppImage, no
PYTHONPATH surgery.
export_fcstdstays gated on FreeCAD:.FCStdis a FreeCAD-Document.xmlarchive with no OCP equivalent, and the CLI does not expose it. The function
keeps its
import FreeCADso anyone calling it directly still works; nothingelse in
pc2cad.pyimports FreeCAD or Part.Blast radius (already mapped)
FreeCAD/Part surface in
pc2cad.py:pc2cad.py:3583_freecad_import— the lazy import shim. Delete it.pc2cad.py:3592_primitive_to_shape(FreeCAD, Part, primitive)— theper-primitive OCP shape constructor. This is the rewrite target, just
stop taking
FreeCAD, Partas parameters.pc2cad.py:4991export_step— uses_freecad_import,Part.makeCompound,Part.export. Rewrite toSTEPControl_Writer.pc2cad.py:5067export_fcstd— keep FreeCAD-gated; document in errormessage; CLI does not expose it.
pc2cad.py:5183_write_stl_binary— already OCP-data-ready; just needsthe upstream data source changed.
pc2cad.py:5212export_stl— uses_freecad_import,compound.tessellate.Rewrite to
BRepMesh_IncrementalMesh+ per-faceBRep_Tool.Triangulation.pc2cad.py:5288compound.tessellate(tol)call site — replaced.Boolean ops to migrate:
shape.fuse/shape.cut/shape.common(8 call sites in
_cut_by_chamfers,_cut_by_fillet_tools,_cut_by_slot_tools,_cut_by_hole_tools,derive_profile_features_from_sections) →BRepAlgoAPI_Fuse/BRepAlgoAPI_Cut/BRepAlgoAPI_Common.Already-working path that converges on this:
pc2cad_profile.py+pc2cad_cli.py::_export_build123d. The fullprofile_extrude→ build123d→ STEP pipeline was verified 2026-06-20 against
samples/sample_plane_cyl.ply(1 solid, 19 faces, 102 edges after STEPround-trip via
OCP.STEPControl_Reader). That is the referenceimplementation.
Verification commands (run after every phase lands)
Acceptance
--out X.stepwrites a real STEP file (>5 KB, valid AP214 header,readable by
OCP.STEPControl_Reader) with no FreeCAD import.--out-format stland--out-format brep.git grep -nE "import (FreeCAD|Part)$" pc2cad.pyreturns no hitsoutside
export_fcstd.tests/test_pipeline.pypasses without FreeCAD test markers; thetest_export_with_freecadtest is either rewritten to use OCP orwrapped in
pytest.importorskip("FreeCAD", reason="FreeCAD-only test").mesh output via
export_residual_mesh(open3d, unchanged)._HAS_FREECADblock, no_freecad_importshim, no conditionalfallback. Decision is final; this is a clean cutover.
Phase plan
P1: Rewrite
_primitive_to_shapeto use OCP constructorsGoal: Replace
pc2cad.py:3592 _primitive_to_shape(FreeCAD, Part, primitive)with an OCP-only equivalent that does not take FreeCAD/Part as parameters.
This is the per-primitive shape constructor every boolean and export op
funnels through, so it is the choke point.
Approach:
BRepPrimAPI_MakeSphere(center, radius).Shape()BRepPrimAPI_MakeCylinder(axis_point, axis_dir, radius, height).Shape()BRepPrimAPI_MakeCone(...)(derive base/apex from primitive)BRepBuilderAPI_MakeFacefrom the polygon'sPoly_Triangulationor from the inlier-hull polygon as aTopoDS_Wireon a plane.
_primitive_to_shape(primitive: Primitive) -> TopoDS_Shape.Drop the FreeCAD, Part params.
Files:
pc2cad.py:3592-3690(rewrite_primitive_to_shape),pc2cad.py:3583-3591(delete_freecad_import).Acceptance:
git grep "def _primitive_to_shape" pc2cad.pyshows the new signature.signature; lint passes.
P2: Migrate boolean ops to OCP (
BRepAlgoAPI_*)Goal: Replace
shape.fuse / shape.cut / shape.commonwith OCP booleanoperations at every call site in
pc2cad.py.Approach: Use
BRepAlgoAPI_Fuse/BRepAlgoAPI_Cut/BRepAlgoAPI_Common. These are not method-chained like FreeCAD's — eachreturns a
BRepAlgoAPI_*object whoseShape()property gives the result.Wrap in a small helper
_fuse(shapes: List[TopoDS_Shape]) -> TopoDS_Shapethat handles the iterative fold.
Files:
pc2cad.py:3809, 3831, 3843, 3851, 3860, 3869, 3925, 3944, 3971, 3978, 4040, 4183— every.fuse / .cut / .commoncall site. Also theboolean ops inside
_cut_by_chamfers,_cut_by_fillet_tools,_cut_by_slot_tools,_cut_by_hole_tools.Acceptance:
git grep -nE "\.fuse\(|\.cut\(|\.common\(" pc2cad.py | grep -v _fuse_helperreturns no hits.
python -m pytest tests/test_pipeline.py -vpasses.python pc2cad_cli.py samples/sample_plane_cyl.ply --out /tmp/p2.stepproduces a valid STEP (boolean ops no longer raise NotImplementedError).
P3: Rewrite
export_stepto useSTEPControl_WriterGoal: Replace
pc2cad.py:4991 export_step(FreeCAD +Part.export)with an OCP
STEPControl_Writerimplementation.Approach:
TopoDS_Shapevia_primitive_to_shape.TopoDS_CompoundviaTopoDS_Builder.STEPControl_Writer:w = STEPControl_Writer();w.Transfer(compound, STEPControl_AsIs);w.Write(path).Files:
pc2cad.py:4991-5066(rewriteexport_step),pc2cad.py:5063-5064(replacePart.makeCompound+Part.export).Acceptance:
python pc2cad_cli.py samples/sample_plane_cyl.ply --out /tmp/p3.stepwrites a STEP file >5 KB.
file /tmp/p3.stepreports "ISO-10303 STEP data".OCP.STEPControl_Readerreads it back without error.P4: Rewrite
export_stlto use OCP mesh + binary writerGoal: Replace
pc2cad.py:5212 export_stl(FreeCAD +tessellate) withan OCP
BRepMesh_IncrementalMesh+ binary writer pipeline. The_write_stl_binaryhelper atpc2cad.py:5183already accepts the(vertices, triangles)shape; just feed it OCP data.Approach:
BRepMesh_IncrementalMesh(compound, tol).BRep_Tool.Triangulation(face, loc)to getPoly_Triangulation(nodes + triangles). Applyloc.Transformation()tonode coordinates.
verticesandtrianglesarrays; pass to
_write_stl_binary.Files:
pc2cad.py:5212-5290(rewriteexport_stl),pc2cad.py:5288-5289(replace tessellate call).Acceptance:
python pc2cad_cli.py samples/sample_plane_cyl.ply --out /tmp/p4.stl --out-format stlwrites a binary STL.python -c "from stl import mesh; m=mesh.Mesh.from_file('/tmp/p4.stl'); print(len(m.vectors))"reports a positive triangle count.FreeCAD-tessellate output for the same sample (±20%).
P5: Wire
export_brepand update CLI / testsGoal: Ensure
export_brep(already OCP-capable viaBRepTools.Write_sper
pc2cad_cli.py:513) is wired into the new pipeline. Updatetests/test_pipeline.pyand add a no-FreeCAD regression test.Approach:
pc2cad_cli.py:513 export_brepuses OCP (it already imports it).If it relies on
pc2cad.pyinternals, refactor to call into the newOCP-only export layer.
tests/test_pipeline.py::test_export_without_freecadthat runs thefull STEP/BREP/STL export on
samples/sample_plane_cyl.plywithoutFreeCAD installed and asserts all three files are non-placeholder.
test_export_with_freecad(if it exists) inpytest.importorskip("FreeCAD", reason="FreeCAD-only test").Files:
pc2cad_cli.py:513, 530, 532,tests/test_pipeline.py.Acceptance:
--out-format brepwrites a valid BREP file with no FreeCAD import.pytest tests/ -vpasses with FreeCAD uninstalled in the test venv.absent.
P6: Final cleanup — delete
_freecad_import, prove no FreeCAD in export pathsGoal: Remove every FreeCAD/Part reference from the export paths.
Verify the acceptance criteria from the original spec are met.
Approach:
pc2cad.py:3583 _freecad_importfunction (no callers remainafter P1-P5).
git grep -nE "import (FreeCAD|Part)$" pc2cad.pyreturns hitsonly inside
export_fcstd(which is intentionally kept gated).for the closing comment.
pointcloud-to-cad-guide.mdto remove FreeCAD installinstructions from the export pipeline section.
Files:
pc2cad.py:3583-3591(delete),pointcloud-to-cad-guide.md(doc update).
Acceptance:
git grep -nE "import (FreeCAD|Part)$" pc2cad.pyreturns hits only inexport_fcstd.pointcloud-to-cad-guide.mdno longer tells users to install FreeCADfor exports.
Out of scope
_fit_*analytic primitive fitting — only the export is broken.pc2cad_profile.pybuild123d path — already working; thisrewrite converges on it, doesn't replace it.
export_residual_mesh— open3d-based, unchanged.expose
export_fcstd; the function still works for direct callers.Related
profile_extrude/profile_revolvenow exported via thenew path).
pc2cad_profile.py+pc2cad_cli.py::_export_build123dare merged and working).Auto-decomposition created
This issue has been automatically decomposed into workable sub-issues.
➡️ Track decomposition at pc2cad#25
Triage: decompose failed
Triage pipeline could not create a sub-issue for auto-decomposition (API error or repo permissions). Marking this
blocked:needs-decisionto prevent re-evaluation. Remove the label to retry.Triage: decompose failed
Triage pipeline could not create a sub-issue for auto-decomposition (API error or repo permissions). Marking this
blocked:needs-decisionto prevent re-evaluation. Remove the label to retry.Triage: decompose failed
Triage pipeline could not create a sub-issue for auto-decomposition (API error or repo permissions). Marking this
blocked:needs-decisionto prevent re-evaluation. Remove the label to retry._primitive_to_shapeto use OCP constructors #41BRepAlgoAPI_*) #42export_stepto useSTEPControl_Writer#43export_stlto use OCP mesh + binary writer #44export_brepand update CLI / tests #45_freecad_import, prove no FreeCAD in export paths #46This issue has been decomposed into the following sub-issues:
pc2cad.py:3592 _primitive_to_shape(FreeCAD, Part, primitive)shape.fuse / shape.cut / shape.commonwith OCP boolean operapc2cad.py:4991 export_step(FreeCAD +Part.export) with anpc2cad.py:5212 export_stl(FreeCAD +tessellate) with an Oexport_brep(already OCP-capable viaBRepTools.Write_sperEach sub-issue has been labeled with
triage-approvedand is ready for queue routing.