# Cad Print 3mf

> Mode-aware .3mf generation alongside STL for build123d projects. Mirrors the design/test/production EXPORT_MODE workflow from cad-build123d-general into Bambu-ready .3mf output: design includes the axis reference cube; test and production are clean meshes. Per-project template convention for slicer-profile-bearing .3mf files. Slicer-side profile mutation is out of scope here; `print-bambu-3mf` covers it when installed. USE FOR: "auto-generate .3mf alongside STL", "export 3mf for design/test/production mode", "hand build123d output directly to Bambu Studio", "skip the STL re-import step", "save mesh metadata about export mode".

- Skill: `rwilson504/cad-print-3mf` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add rwilson504/cad-print-3mf`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rwilson504/cad-print-3mf/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- License: MIT-0
- Author: rwilson504 (https://skillmd.com/u/rwilson504)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/rwilson504/cad-print-3mf

---


# CAD: Mode-aware `.3mf` export from build123d

> **Prerequisites:**
> - `cad-build123d-general` §8a — defines the `EXPORT_MODE` constant 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:

1. **Units / orientation metadata** — STL is just triangles; the slicer
   has to guess at scale.
2. **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:

1. Open Bambu Studio
2. Drag any STL onto the bed (a calibration cube works)
3. Pick the printer + filament profile you'll use for this project
4. Tune walls, infill, supports etc. to your project defaults
5. **Save Project** to `<project>/templates/<material>.3mf`
6. (Done. The `Mesher`-generated bare `.3mf` is 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:

```python
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:

1. Generate the bare `.3mf` (v1 behavior, always happens).
2. Additionally copy the template, replace its mesh with the
   newly-generated mesh, save as `<part>-<profile>.3mf`.
3. In `test` mode, run the swap-result through `apply_test_profile.py
   --profile prototype` to apply the howtogeek prototype recipe.
4. In `production` mode, the swap-result keeps the template's
   production profile.

This is **deferred** because:

- Bambu Studio's `.3mf` mesh format inside `3D/Objects/object_*.model`
  has 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):

```python
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.insert` and 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:

```python
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):

```python
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 on
- `print-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>

