agentsclimarketplace

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

Install
npx -y skills add m-esm/bambu-3mf-export

Assembled 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

  1. Run the project's build.py first so the STLs are current and verified watertight + single-body. Only PRINTED parts go in (bought parts, motors, bearings, screws, clutches, are excluded).
  2. Copy scripts/bambu3mf.py + scripts/bambu_profile_template.json into the project (or keep the project's own copies in sync with this skill).
  3. Adapt scripts/export_bambu_example.pyexport_bambu.py: list parts by category, pack each category onto its own plate(s), collect all plates, then write one multi-plate .3mf.
  4. python3 export_bambu.pya 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 all runs build THEN export (after the assembly_check.py gate from the 3d-print-modeling skill); never leave a geometry fix un-exported. If exports are expensive, have export_bambu.py compare each STL's mtime against its .3mf and print STALE per 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 the SUPPORT dict, 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:0 without 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_pack is 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_width per 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 recorded pos still 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 own write_bambu_3mf final 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-3mf takes 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 --outputdir it 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_deg compares 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 in Metadata/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) with cols = compute_colum_count(n), stride = bed*1.2, same PartPlate.cpp formula as write_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 loses project_settings.config / model_settings.config / subtypes. So bambu3mf.py assembles the ZIP by hand (raw XML strings). Use lib3mf write ONLY for geometry-only output.
  • A Bambu .3mf is standard 3MF plus Bambu-only sidecars. Standard geometry (meshes, transforms, components, build items) is safe through lib3mf; the Bambu layer (negative_part/modifier_part subtypes, 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_part only carves as a sub-part of the same <object>. A standalone object marked negative_part does nothing; it must live inside the target object's <components> AND have a matching <part subtype="negative_part"> in model_settings.config under that object's id. And the slicer does union(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 in build.py instead of fighting subtypes).
  • Three.js 3MFLoader ignores Bambu's multi-file production extension (separate /3D/Objects/object_N.model files) → 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") makes model_settings.config malformed and Bambu Studio fails to load the whole project. Run every interpolated value through _a() (xml.sax.saxutils.escape + &quot;). 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)) (+1 when sqrt(n) > round(sqrt(n))), stride = bed_size * (1 + 1/5) (LOGICAL_PART_PLATE_GAP), plate i origin = (col*stride, -row*stride) with row, col = divmod(i, cols) (rows march toward -Y). Place each part at plate_origin + bed_local_pos. A naive i*(bed+gap) along +X puts 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>
  • type is the CustomGCode enum: 0 ColorChange, 1 PausePrint, 2 ToolChange, 3 Template, 4 Custom. gcode for a pause = the profile's machine_pause_gcode (M400 U1 on A1).
  • The gcode is emitted at the START of the layer whose top equals top_z → set top_z to 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/

assets/

Keep looking

Skills are one crate of 326,537. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.