Tutorial: Reconstructing a Design from an STL
This tutorial describes a practical workflow for using
detect_primitives() to help reconstruct a parametric build123d
model from an STL mesh.
This is not a push-button STL-to-CAD converter. It is a mesh-guided redesign process. The goal is to extract enough analytic structure from a triangulated model to make manual reconstruction faster and more reliable.
警告
Rebuilding a design from STL is usually slow, approximate, and manual.
STL files contain triangles, not modeling intent. Even when
detect_primitives() finds useful planes, cylinders, and
spheres, the final build123d model still needs to be designed deliberately.
Before working through this tutorial, review Steps 1-3 of Designing a Part in build123d. The same ideas apply here:
identify planes of symmetry
identify likely axes of rotation
choose a convenient origin before doing any serious work
These preparation steps often reduce the amount of mesh that needs to be reconstructed to one half, one quarter, or even less.
Overview
The workflow described here is:
Import the STL with
Mesher.Split the mesh by symmetry planes and isolate the region to redesign.
Save that reduced mesh section as a BREP file.
Reload the BREP section while iterating on reconstruction.
Run
detect_primitives().Inspect the returned primitives, leftovers, and generated code.
Rebuild the design intentionally from those clues.
The key output of detect_primitives is guidance:
primitivesshows what was recognized analyticallyleftoversshows what was not coveredcode_linesprovides algebra-mode fragments that often reveal common planes and likely sketch structure
Why Cache a Working Section as BREP?
Importing STL with Mesher is convenient, but large meshes can be
slow to load and process. Once a useful section of the part has been isolated,
save it as a BREP file and use that for repeated experimentation.
BREP files reload much more efficiently in build123d and are better suited to an iterative reconstruction script.
Preparing the Mesh
Start with the STL import and isolate the smallest useful section of the part.
from build123d import *
importer = Mesher()
full_mesh = importer.read("target_part.stl")[0]
# Example: reduce the work to one quarter of a symmetric model
quarter_mesh = split(full_mesh, Plane.YZ)
quarter_mesh = split(quarter_mesh, Plane.XZ)
export_brep(quarter_mesh, "target_part_quarter.brep")
The exact planes depend on the part. The point is not to begin running
detect_primitives on the full mesh if symmetry can remove most of the work.
Reconstruction Script
Once the mesh section has been cached as BREP, iterate on a separate script or enable a reconstruction section of the same script with a Boolean switch.
from build123d import *
working_mesh = import_brep("target_part_quarter.brep")
primitives, leftovers, code_lines = detect_primitives(working_mesh)
print(*code_lines, sep="\n")
This call returns three complementary outputs:
primitivesA
ShapeListof analytic faces that were recognized from the mesh. These are typically planes, cylinders, and spheres. Planar primitives are returned as rectangles sized to the planar region's bounding box, not as the original tessellated mesh patch.leftoversMesh faces that were not matched by the primitive detectors. These indicate freeform regions, noisy regions, or places where manual work is still required.
code_linesGenerated algebra-mode code corresponding to the recognized primitives.
Inspecting the Results
The inspection step is the heart of this workflow.
1. Examine the primitives visually
Look at the returned primitives and decide what the detector found well.
Useful questions include:
Are the expected planar faces present?
Do fillets appear as cylinders?
Do rounded corners appear as spheres?
Are repeated features recognized consistently?
In a simple mechanical part, good output often consists of a small number of common planes, repeated cylinders with similar radii, and only a few leftovers.
2. Examine the leftovers
leftovers show what still needs manual interpretation.
Large leftover regions often indicate one of three things:
the part contains geometry that is not well approximated by planes, cylinders, or spheres
the mesh is noisy or irregular
the working section is still too large to interpret comfortably
If too much of the mesh appears in leftovers, it may be better to refine
the working section, identify more symmetry, or redesign that area manually
instead of trying to automate it further.
3. Examine the generated code
The generated code_lines are intentionally written in Algebra mode and use
Plane * Pos structure to make repeated placement patterns easier to spot.
This often helps answer questions such as:
which faces lie on the same construction plane?
which circles belong to the same sketch?
which cylindrical or spherical regions are repeated instances of one feature?
Treat this code as an annotated report, not necessarily as the final model.
For planar parts in particular, the generated lines are often naturally grouped
by plane. A sequence such as Plane.XY.offset(...) with a few repeated
offset values usually indicates related structure that may belong to one sketch
or one construction stage.
Worked Example: Filleted Box
As a controlled example, consider a filleted box:
fillet_box = fillet(Box(1, 1, 1).edges(), 0.1)
Running detect_primitives on this geometry produces output like:
r00 = Plane.XY.offset(-0.5) * Pos(-0.4, -0.4) * Rectangle(0.8, 0.8, align=Align.MIN)
c01 = Plane.XY.offset(-0.4) * Pos(0.4, 0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c02 = Plane.XY.offset(-0.4) * Pos(-0.4, 0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c03 = Plane.XY.offset(-0.4) * Pos(-0.4, -0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c04 = Plane.XY.offset(-0.4) * Pos(0.4, -0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
r05 = Plane.XY.offset(0.5) * Pos(-0.4, -0.4) * Rectangle(0.8, 0.8, align=Align.MIN)
r06 = Plane.YZ.offset(-0.5) * Pos(-0.4, -0.4) * Rectangle(0.8, 0.8, align=Align.MIN)
c07 = Plane.YZ.offset(-0.4) * Pos(-0.4, -0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c08 = Plane.YZ.offset(-0.4) * Pos(-0.4, 0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c09 = Plane.YZ.offset(-0.4) * Pos(0.4, -0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c10 = Plane.YZ.offset(-0.4) * Pos(0.4, 0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
r11 = Plane.YZ.offset(0.5) * Pos(-0.4, -0.4) * Rectangle(0.8, 0.8, align=Align.MIN)
r12 = Plane.ZX.offset(-0.5) * Pos(-0.4, -0.4) * Rectangle(0.8, 0.8, align=Align.MIN)
c13 = Plane.ZX.offset(-0.4) * Pos(-0.4, 0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c14 = Plane.ZX.offset(-0.4) * Pos(-0.4, -0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c15 = Plane.ZX.offset(-0.4) * Pos(0.4, -0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
c16 = Plane.ZX.offset(-0.4) * Pos(0.4, 0.4) * Face.extrude(Circle(0.0999996).edge(), (0, 0, 0.8))
r17 = Plane.ZX.offset(0.5) * Pos(-0.4, -0.4) * Rectangle(0.8, 0.8, align=Align.MIN)
s18 = Pos((0.399999, -0.399999, 0.400026)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
s19 = Pos((-0.399999, 0.399999, -0.400026)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
s20 = Pos((-0.399999, -0.399999, -0.400026)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
s21 = Pos((0.399999, 0.399999, -0.400026)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
s22 = Pos((-0.399999, 0.400026, 0.399999)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
s23 = Pos((-0.399999, -0.399999, 0.400026)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
s24 = Pos((0.399999, 0.400026, 0.399999)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
s25 = Pos((0.399999, -0.399999, -0.400026)) * Sphere(0.099983).faces().filter_by(GeomType.SPHERE)[0]
This output is informative in several ways:
the six box faces appear as rectangles on three principal planes
the edge fillets appear as cylinders grouped around those same planes
the corner blends appear as spheres near the eight cube corners
The generated code is also structured by plane:
Plane.XY.offset(...)appears with three distinct offsetsPlane.YZ.offset(...)appears with three distinct offsetsPlane.ZX.offset(...)appears with three distinct offsets
That organization is often more useful than any one primitive by itself because it suggests how the model could be regrouped into sketches and construction steps.
Although this output is correct and useful, it still does not represent the best final build123d model. The original design intent is much simpler:
fillet(Box(1, 1, 1).edges(), 0.1)
That is a good example of the main lesson of this tutorial: the generated code helps reveal structure, but the final model should usually be rewritten in a cleaner, higher-level form.
Turning Primitive Hints into Sketches
Once repeated planes become obvious in code_lines, start grouping related
features into sketches and features of your own.
For example:
several rectangles on
Plane.XYmay indicate one base sketch and one or more extrusionsrepeated circles on one plane may indicate hole or boss locations
a collection of cylinders with the same radius may indicate that a fillet or round was part of the original design intent
The generated code is often most useful when treated as:
a list of candidate construction planes
a list of likely sketch elements
a list of repeated primitive sizes and placements
Signs of Good Output
detect_primitives is most helpful when:
the mesh is reasonably clean
the part is mostly mechanical
many surfaces are planar, cylindrical, or spherical
there are clear planes of symmetry or repeated features
In these cases, primitives and generated code often cluster into obvious reconstruction steps.
Signs of Poor Output
Expect more manual work when:
the mesh contains freeform surfaces
the STL is noisy or heavily tessellated
the part has no obvious symmetry
many important regions remain in
leftoversthe generated code contains many tiny or redundant fragments
When this happens, it may be faster to use the mesh only as a visual reference and rebuild the part manually from dimensions and intent.
Summary
The STL reconstruction workflow in build123d is:
analyze the part as a designer, not as a mesh processor
isolate the smallest useful region with symmetry and splitting
cache that region as BREP
inspect primitives, leftovers, and code
rebuild the part intentionally in build123d
The most useful mindset is to treat detect_primitives as a design assistant.
It can show where the planes, cylinders, and spheres probably are, but the
final parametric model still comes from careful human interpretation.