mirror of
https://github.com/gumyr/build123d.git
synced 2026-05-09 13:42:32 -07:00
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-24.04-arm, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.14) (push) Waiting to run
tests / tests (macos-15-intel, 3.14) (push) Waiting to run
tests / tests (ubuntu-24.04-arm, 3.14) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.14) (push) Waiting to run
tests / tests (windows-latest, 3.14) (push) Waiting to run
Run type checking / typecheck (3.10) (push) Waiting to run
Run type checking / typecheck (3.14) (push) Waiting to run
298 lines
12 KiB
ReStructuredText
298 lines
12 KiB
ReStructuredText
.. _stl_reconstruction_tutorial:
|
|
|
|
#############################################
|
|
Tutorial: Reconstructing a Design from an STL
|
|
#############################################
|
|
|
|
This tutorial describes a practical workflow for using
|
|
:func:`~build123d.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.
|
|
|
|
.. warning::
|
|
|
|
Rebuilding a design from STL is usually slow, approximate, and manual.
|
|
STL files contain triangles, not modeling intent. Even when
|
|
:func:`~build123d.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
|
|
:ref:`design_tutorial`. 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 :class:`~mesher.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 :func:`~build123d.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 :class:`~mesher.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.
|
|
|
|
.. code-block:: build123d
|
|
|
|
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.
|
|
|
|
.. code-block:: build123d
|
|
|
|
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 :class:`~topology.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:
|
|
|
|
.. code-block:: build123d
|
|
|
|
fillet_box = fillet(Box(1, 1, 1).edges(), 0.1)
|
|
|
|
Running ``detect_primitives`` on this geometry produces output like:
|
|
|
|
.. code-block:: build123d
|
|
|
|
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:
|
|
|
|
.. code-block:: build123d
|
|
|
|
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 :func:`~build123d.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.
|