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:

  1. Import the STL with Mesher.

  2. Split the mesh by symmetry planes and isolate the region to redesign.

  3. Save that reduced mesh section as a BREP file.

  4. Reload the BREP section while iterating on reconstruction.

  5. Run detect_primitives().

  6. Inspect the returned primitives, leftovers, and generated code.

  7. Rebuild the design intentionally from those clues.

The key output of detect_primitives is guidance:

  • primitives shows what was recognized analytically

  • leftovers shows what was not covered

  • code_lines provides 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:

primitives

A ShapeList of 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.

leftovers

Mesh faces that were not matched by the primitive detectors. These indicate freeform regions, noisy regions, or places where manual work is still required.

code_lines

Generated 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 offsets

  • Plane.YZ.offset(...) appears with three distinct offsets

  • Plane.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.XY may indicate one base sketch and one or more extrusions

  • repeated 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 leftovers

  • the 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:

  1. analyze the part as a designer, not as a mesh processor

  2. isolate the smallest useful region with symmetry and splitting

  3. cache that region as BREP

  4. run detect_primitives()

  5. inspect primitives, leftovers, and code

  6. 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.