CAD: Mode-aware .3mf export from build123d
Prerequisites:
cad-build123d-general§8a — defines theEXPORT_MODEconstant and the design / test / production workflow this skill plugs into.Optional, not required:
print-bambu-3mf(separate plugin) is the slicer-side companion. This skill produces the.3mf; that one mutates it (prototype/draft profiles) and diffs/templates Bambu project files. Without it, use the manual template workflow below — nothing here depends on it.
Purpose
By default, cad-build123d-general exports STL files. STL is fine for
sharing geometry, but it loses two things that matter for printing:
- Units / orientation metadata — STL is just triangles; the slicer has to guess at scale.
- A direct path into Bambu Studio's project workflow — STL imports as a fresh object every time, losing the slicer profile you tuned for this part last week.
.3mf solves both: it embeds units, can carry per-object metadata, and
is Bambu Studio's native project format. This skill adds automatic
mode-aware .3mf output alongside STL, so every CAD rebuild emits
both files and you can drag the .3mf directly into Bambu Studio.
Mode mapping
The EXPORT_MODE constant from cad-build123d-general §8a drives both
the STL and the .3mf outputs in lockstep:
EXPORT_MODE |
STL behavior (existing) | .3mf behavior (new) |
|---|---|---|
design |
STL with axis reference cube exported beside the part | .3mf with axis reference cube as a second object (drag both into slicer to verify orientation) |
test |
Clean STL with on-part wall labels | Clean .3mf, no axis cube (the on-part labels survive in the mesh) |
production |
Clean STL + *.print.md sidecar |
Clean .3mf + the same sidecar (sidecar applies to both files) |
All modes embed cad_print_3mf_mode metadata inside the .3mf so a
human inspecting the file with unzip -p file.3mf 3D/3dmodel.model | head can see which export mode produced it.
Per-project template convention
.3mf files generated by build123d's Mesher are bare 3MFs —
geometry only, no slicer profile. Bambu Studio opens them, but treats
each as a fresh import (you lose any tuned profile).
For real print iteration, you want a profile-bearing template — a
.3mf saved out of Bambu Studio with your tuned settings (material,
layer height, walls, supports). Convention:
<project>/
├── exports/
│ ├── my_part.stl # always written
│ ├── my_part.3mf # always written (bare 3MF)
│ └── my_part.print.md # production mode only
└── templates/
├── p2s_pla.3mf # one per (printer, material) you use
├── p2s_petg.3mf
└── p2s_abs.3mf
Saving a template is a one-time, manual Bambu Studio operation:
- Open Bambu Studio
- Drag any STL onto the bed (a calibration cube works)
- Pick the printer + filament profile you'll use for this project
- Tune walls, infill, supports etc. to your project defaults
- Save Project to
<project>/templates/<material>.3mf - (Done. The
Mesher-generated bare.3mfis your day-to-day output; the template only matters for v2 below.)
v1 (this skill ships): bare .3mf mode-mirroring
The reference helper scripts/cad_print_3mf.py provides:
from cad_print_3mf import export_3mf_for_mode
# After (or instead of) export_with_reference(part, stl_path, ...)
export_3mf_for_mode(
part,
stl_path=stl_path,
export_mode=EXPORT_MODE,
bed_face=BED_FACE,
)
Behavior matches the table above. The .3mf lands at the same path as
the STL with the extension swapped. Mesher metadata embedded:
| Metadata key | Value |
|---|---|
cad_print_3mf_mode |
design / test / production |
cad_print_3mf_part_number |
The part number you passed (or auto-derived from the STL filename) |
cad_print_3mf_bed_face |
The bed_face value (e.g. -Z) |
v2 (deferred): template mesh-swap
Once you have a profile-bearing template saved, the helper can:
- Generate the bare
.3mf(v1 behavior, always happens). - Additionally copy the template, replace its mesh with the
newly-generated mesh, save as
<part>-<profile>.3mf. - In
testmode, run the swap-result throughapply_test_profile.py --profile prototypeto apply the howtogeek prototype recipe. - In
productionmode, the swap-result keeps the template's production profile.
This is deferred because:
- Bambu Studio's
.3mfmesh format inside3D/Objects/object_*.modelhas vendor-specific quirks (UUID/path references, namespace declarations, transform matrices) that we haven't validated against a real saved file. - Shipping mesh-swap code without a real Bambu template to test it against guarantees a "works in dev, breaks the printer" bug.
When you save your first real template, drop the path in this skill's "Templates verified" section below and we'll wire up v2.
Templates verified
(None yet — list templates that have been confirmed to round-trip
through apply_test_profile.py here as they're added.)
Reference helper API
scripts/cad_print_3mf.py (v1):
def export_3mf_for_mode(
part, # build123d Part / Compound / Solid
*,
stl_path: Path, # source-of-truth path; .3mf derived from it
export_mode: str, # "design" | "test" | "production"
bed_face: str | None = None, # axis cube label in design mode
axis_ref_block: Part | None = None, # if None and design mode, no cube added
part_number: str | None = None,
metadata: dict[str, str] | None = None,
) -> Path:
"""Write a .3mf next to the STL with mode-aware contents.
Returns the path to the .3mf written.
"""
The script imports build123d.Mesher for the heavy lifting and stays
under 100 lines. Project scripts are expected to either:
- Inline-copy the helper into the project script (preferred — keeps the project self-contained, matches the existing repo pattern), or
sys.path.insertand import the helper from the skill folder (handy for one-off experimentation; brittle for committed code).
Integration with cad-build123d-general §8a
The existing pattern:
def export_with_reference(part, filename, *, offset_x=0, offset_y=0,
offset_z=0, bed_face=None):
if EXPORT_MODE == "design":
ref_block = make_axis_reference_block(bed_face=bed_face)
ref_block = ref_block.move(Location((offset_x, offset_y, offset_z)))
combined = Compound(children=[part, ref_block])
export_stl(combined, filename)
else:
export_stl(part, filename)
Becomes (additive — STL still exported):
def export_with_reference(part, filename, *, offset_x=0, offset_y=0,
offset_z=0, bed_face=None):
# ... existing STL writing unchanged ...
if EXPORT_3MF: # new module-level constant
ref_block = (make_axis_reference_block(bed_face=bed_face)
.move(Location((offset_x, offset_y, offset_z)))
if EXPORT_MODE == "design" else None)
export_3mf_for_mode(
part, stl_path=Path(filename),
export_mode=EXPORT_MODE, bed_face=bed_face,
axis_ref_block=ref_block,
)
Add EXPORT_3MF = True to the project's parameter block. Toggle off
for projects where the STL alone is enough (e.g. shared-online models,
non-FDM workflows).
When to use which file
| Scenario | Use |
|---|---|
| Day-to-day rebuild, eyeball in OCP CAD Viewer | The build123d show() call (no file at all) |
| Drag into Bambu Studio for a one-off slice | The bare .3mf from this skill |
| Daily-driver print with your tuned profile | Drag the bare .3mf over the model in your saved template .3mf |
| First test print after a CAD change | Drag the bare .3mf onto a prototype template .3mf; or automate it with apply_test_profile.py --profile prototype from print-bambu-3mf (separate plugin) |
| Sharing the model online | The STL (universal) + the *.print.md sidecar |
| Sending to printer | .gcode.3mf exported by Bambu Studio after slicing |
Common gotchas
| Symptom | Cause | Fix |
|---|---|---|
Bambu Studio opens the .3mf but loses my profile |
The bare .3mf from Mesher has no Metadata/project_settings.config |
Drag it onto your template .3mf instead of opening it directly; or re-save through Bambu Studio with your template selected |
| Axis ref cube prints alongside the part in test mode | Helper called with axis_ref_block=... while export_mode == "test" |
Pass axis_ref_block=None (or check the mode) — the helper trusts what you give it |
Sidecar exists for STL but not .3mf |
Sidecar generation lives in the project script's main(), not the 3MF helper |
Run sidecar generation once per part; the same sidecar applies to both STL and .3mf |
.3mf imports at wrong scale into the slicer |
Mesher defaults to mm; slicer is set to inches |
Set slicer units to mm; or pass unit=Unit.MM when constructing Mesher() |
| Build123d crash: "no shapes added before write()" | Forgot to call add_shape(part) before write(path) |
Always add_shape then write |
Compound passed to add_shape flattened to one mesh |
Mesher treats a Compound as one shape; for "two objects in one .3mf" pass a list |
add_shape([part_a, part_b]) instead of add_shape(Compound(children=[a, b])) |
See Also
cad-build123d-general§8a — design/test/production workflow this builds onprint-bambu-3mf(separate plugin) — slicer-side mutation tooling (apply_test_profile.py)print-bambu-studio(separate plugin) — the slicer settings that go into a profile-bearing template- 3MF Consortium spec: https://github.com/3MFConsortium/spec_core
- build123d Mesher API: https://build123d.readthedocs.io/en/latest/imports.html#three-d-imports-and-exports