Bambu 3mf export
Skill m-esm/bambu-3mf-export
Export finished STL meshes to a real Bambu Studio .3mf PROJECT with print settings baked in, no "not from Bambu Lab, load geometry data only" warning, support/infill/profile already set. Use when the user wants a sliceable/printable file for a Bambu Lab printer (A1/A1 Mini/P1/X1), to "export to 3mf", bake print settings, split parts across plates, or set up FINE/FAST print profiles. Also the reference for Bambu .3mf internals and lib3mf gotchas. Pairs with the 3d-print-modeling skill.From its SKILL.md
npx -y skills add m-esm/bambu-3mf-exportAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
19.5 KB, ~5.3k tokens by cl100k_base, as published. Nobody here has run it
Bambu .3mf export (settings baked in)
A plain trimesh/lib3mf .3mf carries geometry only, so Bambu Studio shows "The 3mf is not
from Bambu Lab, load geometry data only" and you re-enter every setting by hand. This skill writes
the same package layout Bambu Studio itself writes, so the file opens as a real project with the
process / filament / printer + per-object support, infill, and brim already set.
How to use
- Run the project's
build.pyfirst so the STLs are current and verified watertight + single-body. Only PRINTED parts go in (bought parts, motors, bearings, screws, clutches, are excluded). - Copy
scripts/bambu3mf.py+scripts/bambu_profile_template.jsoninto the project (or keep the project's own copies in sync with this skill). - Adapt
scripts/export_bambu_example.py→export_bambu.py: list parts by category, pack each category onto its own plate(s), collect all plates, then write one multi-plate.3mf. python3 export_bambu.py→ a single project file (e.g.exports/bambu_klonk.3mf) with every plate as a Studio tab. Gitignore the.3mf(regenerable).
ALWAYS one multi-plate .3mf (do not emit N separate files)
Default and required pattern: one Bambu Studio project containing all plates.
plates = []
for cat, items in CATEGORIES:
for i, parts in enumerate(shelf_pack(items), 1):
plates.append({"name": cat if n == 1 else f"{cat} {i} of {n}", "parts": parts})
write_bambu_3mf("exports/bambu_klonk.3mf", plates, PROJECT_SETTINGS)
- Do not write
bambu_fine1.3mf,bambu_housing1.3mf, … as separate projects unless the user explicitly asks for split files (rare). - Report a plate manifest (tab name + part list) and an EXCLUDED (bought) line every export.
- Plate names must be filename-safe: no
/ \ : * ? " < > |(use"Housing 2 of 2", not"Housing (2/2)").
write_bambu_3mf(path, plates, overrides, plate_name=None) is the one call you need. plates is a
list of {name, parts:[...]}, where each part is {name, mesh, pos:(x,y), obj_settings:{...}}. The
writer lays plates on Bambu's PartPlate grid (cols = round(sqrt(n)), stride = bed*1.2, rows
toward −Y), maps them to plater_ids 1..N, centres each part on its bed, and drops it to z=0.
Backward-compat: pass a flat list of part dicts and it is treated as one plate (named via
plate_name). overrides are project_settings.config keys (one global process/filament/printer
profile per project); obj_settings are per-object keys in model_settings.config.
Keep exports current, and say what's in them (staleness is a recurring user pain)
Users repeatedly could not tell whether a .3mf matched the current geometry ("did you
export?" asked in multiple sessions; a worm STL regenerated after its plate was written;
a push_pin_x6.stl that contained ONE pin; bought parts silently absent from plates the
user was scanning for them). Rules:
- Export is part of the build.
make allruns build THEN export (after theassembly_check.pygate from the 3d-print-modeling skill); never leave a geometry fix un-exported. If exports are expensive, haveexport_bambu.pycompare each STL's mtime against its.3mfand printSTALEper plate, and check that before handing over. - Print a manifest on every export, and show it to the user: which parts landed on which plate/file (with quality + filament group), part counts per plate (catches the x6-that-is-x1 class), and an explicit "EXCLUDED (bought): motor, 608 x2, M3 screws" line. Deliberate exclusions the user can't see read as missing parts.
- After any geometry change to an already-exported part, name the affected plates in your reply ("worm changed -> re-exported bambu_petg_fine1.3mf; other plates untouched").
The two-profile strategy (fast + light bulk)
Split parts so only detail needs fine layers:
- FINE, gears, threads, contact surfaces: 0.16 mm, 4 walls, denser infill (25–50 % on load-bearing rims/races).
- FAST, housings, enclosures, big simple parts: 0.28 mm layer (~40 % less print TIME), 3 walls, 10 % lightning infill, with per-object overrides bumping structural parts (bearing/ motor seats) back to 4 walls / 15 %.
On one measured leg, FAST-style settings (0 % infill + 0.28 mm + 2 walls) cut −44 % time / −38 % filament vs the 15 %/0.20 default. The layer height is the big time lever; infill + walls are the material levers.
A third named profile shows up often enough to keep around:
- CLEAR, a see-through PETG lid / window: 0.16 mm, 100 % infill, and
ironing_type: "top"(+ironing_flow,ironing_spacing) for a glassy top face. Transparent PETG only reads clear with solid infill, sparse infill is opaque and ugly through the wall. (A clear hollow chamber is the opposite case: 0 % infill + a thin defined shell + a tapered self-supporting roof, see the fdm-design-rules hollowing note.)ironing_type: "top"is also the move for any flush-engraved top surface, e.g. an angle scale recessed into a face, so it reads cleanly.
Group plates by filament too, not only by quality
Quality (FINE/FAST/CLEAR) and material/color are independent axes. The A1 prints one filament
per job (single nozzle), so when parts span colors or materials the real split is by filament. The
turntable shipped four filament groups: white PLA (top plate), black PLA (shell + cradle), red
PETG-fine (gears, threads), red PETG-med (bulk mechanism). Each group is its own *_SETTINGS dict
(own filament + nozzle/bed temps + the right quality) fed through the same emit(); name the
outputs bambu_<material><n>.3mf so you know which spool to load. You usually pick one filament
group AND one quality per plate (e.g. red PETG at FINE for gears, black PLA at FAST for the shell).
True multi-color within one plate is a different mechanism: it needs the AMS and a per-object
filament_id in model_settings.config (more config, and a real wipe-tower time/material cost). For
single-nozzle, one-color-per-plate, don't reach for that, just group the parts and emit separate files.
A bigger nozzle (0.6 / 0.8 mm) is not a one-key change
Setting nozzle_diameter: "0.8" alone makes perimeter-heavy parts slower: flow stays capped at the
0.4 profile's filament_max_volumetric_speed (~12 mm³/s), so wider lines just take longer per mm. To
get the time win: raise filament_max_volumetric_speed to ~21, scale the *_line_width keys to the
nozzle, go coarser on layer_height (0.5–0.6), and set print_compatible_printers to the
0.8-nozzle machine or Studio refuses the plate with "not compatible". The catch: a 0.8 nozzle's
±0.25 mm tolerance swamps a designed 0.1–0.2 mm interference, so press-fits become glue-fits, keep
bearing seats and snap pins on a 0.4 plate, run the bulk structural parts on the 0.8. (Design side of
this is in the fdm-design-rules reference.)
Support: per-object, auto tree, always check
- Bambu honors support PER-OBJECT (
model_settings.config), NOT the global project flag from a hand-built.3mf. The example stamps support onto every object via theSUPPORTdict, keep that. - Auto tree support ON is the safe default. It builds only under real overhangs and leaves
self-supporting faces (open-top cavities, ramped catches, flat-top-down lids) bare, and peels off
easily. Disabling support globally to "save time" silently fails the parts that needed it
(enclosure roofs, wire holes, recess ceilings). Don't set
enable_support:0without checking each part's overhangs.
Orientation + packing
- Load each part in its print orientation: lids and flat-faced detail parts flipped top-down
(
FLIP_X) so the smooth face / detail lands on the bed and cavities open upward (self-supporting). shelf_packis a first-fit-decreasing nest: turns parts landscape (longest edge along +X), rows tallest-first, opens a new plate when full. The export bed-checks every part (flags "OFF PLATE").- Pack brim-aware, not bounds-aware. The global profile gives every part an outer brim, so nest
the brim-grown footprint (
bbox + 2·brim_widthper axis), not the raw mesh bounds, and keep a small air gap (~6 mm) between footprints. The part centre = footprint centre (a symmetric outer brim stays centred), so the recordedposstill lands the mesh in-bed. Skip this and neighbouring brims merge into one blob and the bed-check passes a part whose brim actually hangs off the plate.
BambuStudio-CLI auto-arrange (and why auto-orient is OFF by default)
scripts/bambu_autopack.py feeds a finished multi-plate project back through BambuStudio's
own orient/arrange engines (the code behind the GUI buttons). Verified on 02.08.00.50.
Two entry points:
autopack(path, orient=, arrange=, deliberate=, categories=), global pass, atomically replaces the file, renames plates from contents (arrange moves parts across plates, so input plate names go stale). Side gain: the CLI output carries real plate thumbnails.packed_plates(path, deliberate=, brim=), arrange a SINGLE-CATEGORY temp project and return the packed layout ({name, mesh, pos}per plate) for your ownwrite_bambu_3mffinal assembly. This keeps plates category-pure (a global arrange mixes coupon strips with head shells and kills selective printing) while the packing quality is Bambu's.
Findings that must survive (all measured, desk-pi 2026-07-15):
--export-3mftakes a BARE filename and REQUIRES--outputdir. The CLI prefixes the export name with the outputdir; an absolute path becomes<outputdir>/<abs-path>and fails, and without--outputdirit exits 0 having written NOTHING. The output FILE existing is the only reliable success signal, not the exit code.- Auto-orient is blind to your constraints. Never blanket-trust it. On a project
where every pose was deliberate it wanted to flip 16 parts: a cosmetic top face into
tree supports, print-in-place master links onto their floating C-jaw (a known print
failure), a Ø5x209 rod stood on end. It DOES fix genuinely bad poses (a sideways bolt
got stood head-down), so it's useful for parts you haven't thought about, and as a
VALIDATOR.
deliberate=("*",)(fnmatch patterns) hard-fails if a matching part gets TILTED; pure yaw from arrange is always allowed (_tilt_degcompares the +Z axis, so packing rotations pass the audit). - Auto-arrange is outline-aware but BRIM-BLIND (no CLI spacing option): it nests
beautifully (threads a rod through a ring's bore) but packs to ~1 mm gaps, so two 5 mm
brims would merge. Fix baked into
packed_plates(brim=...): concatenate a 0.1 mm-thin sacrificial pad under each mesh = footprint buffered by the brim width, arrange spaces the pads, strip them on read-back (_strip_pad, disconnected components with z extent ~pad height). The buffer is a Minkowski sum, so the padded bbox centre coincides with the mesh bbox centre at any yaw and read-back positions stay exact. - Best-of-both, not Bambu-always: measured on 9 categories, Bambu's engine beat the brim-aware shelf nest on many-small-part categories (48 fasteners: 2 plates -> 1; 8 link strips: 3 -> 2) and LOST on few-big-part ones (its bed margins put a 6-plate chassis on 7, a 5-plate head set on 7). Compute both, keep whichever needs fewer plates, tie to the naive nest (known layout, zero read-back risk).
- Output is production-extension format: meshes split into
3D/Objects/object_N.model(referenced via<component p:path>), object ids RENUMBERED from 1. Map objects input->output by NAME-OCCURRENCE ORDER (names + per-object settings are preserved inMetadata/model_settings.config, document order is stable); ids are useless as keys. - Read-back grid = the writer's grid: plate i origin
(col*stride, -row*stride)withcols = compute_colum_count(n),stride = bed*1.2, same PartPlate.cpp formula aswrite_bambu_3mf; assert every read-back part lands within its plate cell. - Multi-body objects (print-in-place strips) survive the round trip with hinge gaps and body counts intact, but assert counts after read-back anyway.
Group + name plates by category (selective printing)
Don't dump all parts onto sequential "Plate 1/2/3". Group by function/category and name each plate
after its category, so the Bambu Studio plate tabs read "Bolts", "Connectors", etc. and a user can
print just one category by printing one plate. In this project the category map lives in the viewer
(tools/viewer.py CATEGORIES + CAT_ORDER); the exporter imports it (from viewer import ...) so
the two never drift, define the grouping once. Pattern: bucket parts by category, shelf_pack each
bucket independently onto its own plate(s), and when a category overflows the bed name the spill
"Cat (1/2)", "Cat (2/2)"; a single-plate category stays just "Cat". This composes with the
filament-grouping axis below: group by filament first (one spool per job), by category within that.
PETG / water parts
Override the profile to Generic PETG @ A1: 240/250 °C nozzle, 80 °C textured-PEI bed, reduced cooling (none for the first ~3 layers), 8 mm outer brim, slow ~20 mm/s first layer, ~0.15 mm elephant-foot comp, and for watertight: 6 wall loops + 8 top / 8 bottom shells, ~40 % infill on chunky bosses. Print on a clean plate (wipe PEI with IPA, skin oils kill PETG grip). See the fdm-design-rules reference in the 3d-print-modeling skill for the warp/adhesion story.
What makes a .3mf "a Bambu project" (reverse-engineered)
The package (a ZIP) needs:
3D/3dmodel.model, meshes + the production ('p') extension (p:UUID on objects/build items) +<metadata name="Application">BambuStudio-....Metadata/project_settings.config, the ~557-key process/filament/printer JSON. Start from a validated template lifted from a real Studio export (bambu_profile_template.json, an "0.20mm Standard @BBL A1 / Generic PLA" baseline) and override only the keys you care about.Metadata/model_settings.config, object names, the<plate>mapping, per-object overrides.Metadata/slice_info.config,[Content_Types].xml,_rels/.rels, package glue.
lib3mf / .3mf gotchas (the reason we hand-assemble the ZIP)
- lib3mf silently DROPS Bambu sidecar configs on write. A
model.QueryWriter("3mf").WriteToFile()round-trip losesproject_settings.config/model_settings.config/ subtypes. Sobambu3mf.pyassembles the ZIP by hand (raw XML strings). Use lib3mf write ONLY for geometry-only output. - A Bambu
.3mfis standard 3MF plus Bambu-only sidecars. Standard geometry (meshes, transforms, components, build items) is safe through lib3mf; the Bambu layer (negative_part/modifier_partsubtypes, plate config, slicer profile, thumbnails) needs raw ZIP+XML. - Object IDs renumber, Bambu reassigns top-level IDs when objects are added/removed in Studio. Don't hardcode them across sessions; re-inspect first. (The writer here assigns ids from 2 up.)
- Two transform conventions coexist in one file.
3dmodel.model<item>/<component>transform= 12 floats (row-major 4×3, translation in the last 3).model_settings.config<metadata key="matrix">= 16 floats (row-major 4×4, translation in the 4th column of each row). Same effective transform, different shape, don't paste between them. negative_partonly carves as a sub-part of the same<object>. A standalone object markednegative_partdoes nothing; it must live inside the target object's<components>AND have a matching<part subtype="negative_part">inmodel_settings.configunder that object's id. And the slicer doesunion(positives) − union(negatives)in one pass, so a positive feature placed inside a negative cavity gets subtracted away, redefine the cavity mesh to exclude that volume (do it as CSG inbuild.pyinstead of fighting subtypes).- Three.js
3MFLoaderignores Bambu's multi-file production extension (separate/3D/Objects/object_N.modelfiles) → gives an empty scene. For the viewer, bake to GLB. - Hand-assembled XML must escape attribute values. Plate/part/category names get interpolated
straight into
value="..."attributes. An unescaped&,<,>, or"(e.g. plate name "Nuts & inserts") makesmodel_settings.configmalformed and Bambu Studio fails to load the whole project. Run every interpolated value through_a()(xml.sax.saxutils.escape+"). Numeric override values are safe, names are not. - Plate/object names are validated as filenames. Bambu Studio rejects
< > : / \ | ? * "in a plate or object name with "Invalid name, the following characters are not allowed". This bites the obvious"Cat (1/2)"plate label, the/is illegal. Use"Cat 1 of 2". (&is allowed by the name validator but still needs XML-escaping per the rule above.) - Multi-plate: match Bambu's PartPlate grid or parts float off the plates. Each object's world
coords decide which plate cell it's drawn in. Bambu lays plates out as a grid, NOT a single row:
cols = compute_colum_count(n)=round(sqrt(n))(+1whensqrt(n) > round(sqrt(n))),stride = bed_size * (1 + 1/5)(LOGICAL_PART_PLATE_GAP), plateiorigin =(col*stride, -row*stride)withrow, col = divmod(i, cols)(rows march toward-Y). Place each part atplate_origin + bed_local_pos. A naivei*(bed+gap)along+Xputs every part in one line hovering off the plate grid. (Source:src/slic3r/GUI/PartPlate.cpp,compute_shape_position.)
Headless slicing for measurement (BambuStudio CLI)
To actually measure print time / filament (not estimate), see the foreign-cad-import skill's
"BambuStudio CLI" section, flatten the inherits profile chain, set curr_bed_type to
"Textured PEI Plate" for PETG, put overrides in the flattened process JSON, then
BambuStudio --load-settings "m.json;p.json" --load-filaments "f.json" --slice 0 --outputdir OUT model.stl.
Filament-change pauses without AMS (verified 2026-07, torque-lever project)
Single-nozzle two-color (e.g. black engraving floors under a white top face): bake a
pause into the project by appending Metadata/custom_gcode_per_layer.xml to the ZIP
after write_bambu_3mf() (plain zipfile.ZipFile(path, "a").writestr(...), no
content-types registration needed). Schema from bbs_3mf.cpp _add_custom_gcode_per_print_z_file_to_archive:
<?xml version="1.0" encoding="utf-8"?>
<custom_gcodes_per_layer>
<plate>
<plate_info id="1"/>
<layer top_z="5.6" type="1" extruder="1" color="" extra="" gcode="M400 U1"/>
<mode value="SingleExtruder"/>
</plate>
</custom_gcodes_per_layer>
typeis the CustomGCode enum: 0 ColorChange, 1 PausePrint, 2 ToolChange, 3 Template, 4 Custom.gcodefor a pause = the profile'smachine_pause_gcode(M400 U1on A1).- The gcode is emitted at the START of the layer whose top equals
top_z→ settop_zto the FIRST layer of the NEW color (engrave floors at z=F, 0.2 layers →top_z = F + 0.2). Make engrave depth an exact layer multiple (0.6 = 3 × 0.20). - One
<plate>block per plate,plate_info id= 1-based plater id; multiple<layer>entries per plate are allowed.
What ships with it: 11 files
1459.7 KB alongside SKILL.md, 3 of them executable
.claude-plugin/
- marketplace.json672 B
- plugin.json589 B
assets/
- plates.png144.6 KB
- viewer.png1227.4 KB
scripts/
- bambu3mf.pyruns8.3 KB
- bambu_autopack.pyruns16.0 KB
- bambu_profile_template.json48.8 KB
- export_bambu_example.pyruns7.1 KB
- .gitignore90 B
- LICENSE1.0 KB
- README.md5.1 KB