Skip to content

feat(3mf): preserve per-part colours and extruder assignments through texturing - #111

Open
jcarranz97 wants to merge 1 commit into
CNCKitchen:mainfrom
jcarranz97:feat/3mf-color-preservation
Open

feat(3mf): preserve per-part colours and extruder assignments through texturing#111
jcarranz97 wants to merge 1 commit into
CNCKitchen:mainfrom
jcarranz97:feat/3mf-color-preservation

Conversation

@jcarranz97

Copy link
Copy Markdown

Fixes #110

First contribution here, thanks for building this. I ran into this while texturing a multi-material lip balm tube: the model was authored in Bambu Studio as three parts on three extruders, and after texturing in BumpMesh everything came back as one untinted body that had to be repainted by hand before slicing.

This teaches the pipeline to carry per-part colour and extruder data from import all the way to export, and adds a viewport toggle so the part layout is visible while you work. Models without colour data are completely unaffected: they take the same code paths and produce byte-identical output to before.


What changed and why

Reading the colour data at import

A slicer-produced 3MF keeps this information in two places, and neither was being read. The core spec side is a <basematerials> resource holding a displaycolor per body, with each <object> pointing into it via pid and pindex. The part to extruder mapping, which is what actually matters for a multi-tool print, lives in Metadata/model_settings.config, a Bambu Studio and OrcaSlicer convention that sits outside the core spec.

The importer now parses both and attaches a palette plus a per-triangle slot array to the geometry. Because model_settings.config is not part of the spec, it is treated as optional and basematerials acts as the fallback.

One subtlety worth calling out: the load path drops degenerate and NaN triangles by compacting the position array, which renumbers every face. The per-triangle array is compacted in lockstep, otherwise every slot would silently shift.

Carrying it through the pipeline

Reading the data at import and writing it at export is not sufficient on its own, because subdivision, regularisation, decimation and T-junction repair all rewrite the triangle set. Rather than re-deriving the assignment spatially at the end, which would be both slow and approximate at part boundaries, each stage now carries the slot forward:

  • Subdivision and regularisation already tracked a faceParentId map, so slots are projected through it. Export mode now composes that map too, since previously only bake mode needed it.
  • Displacement preserves triangle count and order, so it passes through untouched.
  • Decimation keeps each surviving face's own slot, and T-junction repair propagates the parent slot to every shard of a split face.

Decimation needed one extra guard. buildIndexed welds coincident vertices globally, so two touching parts share the vertices along their contact surface. Collapsing across that boundary would drag one filament's geometry into the other's and visibly ragged the colour seam, so vertices seen by more than one material are pinned, the same treatment locked faces already get. The cost is slightly less aggressive decimation right at part seams, which seemed like the right trade.

Writing a multi-part 3MF

When part data is present, the exporter now emits a <basematerials> resource, one <object> per part with its own mesh, a components assembly tying them together, and a Metadata/model_settings.config carrying the extruder assignments. Vertex deduplication happens per part, since 3MF vertex indices are object local and separate vertex lists are what make the parts independent bodies rather than one welded shell. Part names come from the imported file, so they are XML escaped.

The package deliberately mirrors what Bambu Studio itself writes, including its omission of a content type declaration for model_settings.config. Matching a known good file seemed safer than being strictly OPC correct here, but happy to add an <Override> if you would rather.

Without part data the exporter takes the original path unchanged: a single <object id="1">, no basematerials, no config entry.

Part Colours viewport toggle

The viewport previously gave no hint that a model had parts at all, since every mesh renders in the same flat teal. A new toggle in the viewport footer, shown only for a multi-colour import and enabled automatically on load, shades each body instead, with a small legend mapping colour to part name and tool.

Colours are keyed on the extruder number rather than the file's displaycolor. Files routinely give several parts the same swatch (white is a common default) while still printing them on different tools, so colouring by swatch would render those parts identical here even though the slicer shows them apart. Keying on the tool means what you see groups the same way it will print. Parts with no extruder fall back to their displaycolor, then to neutral grey.

Masked and include-only regions keep their part hue underneath the mask tint rather than going flat orange, so the colour layout stays readable while painting. Saturated orange is deliberately absent from the palette, since that is the exclusion mask colour.

The whole thing is one partColor vertex attribute plus a uniform, so it works on the base mesh, the 3D displacement preview and the precision masking mesh through the same parent maps the masking code already uses.


Testing

There is no automated test suite in the repo, so verification was manual, done in a browser against a real Bambu Studio 3MF with three parts on extruders 1, 2 and 3.

Full round trip. Imported the file, confirmed the palette parsed correctly (three parts, correct names, colours and extruders), ran the complete export pipeline including subdivision, regularisation, displacement, decimation from 61,336 down to 8,000 triangles, and T-junction repair, then unzipped the exported package and confirmed it contained the basematerials resource, three part objects with correct pid and pindex, the components assembly and a model_settings.config with extruders 1, 3 and 2 matching the source. Re-imported the exported file and confirmed all three parts came back with identical names, colours and extruders.

Through the real UI. Repeated the same flow through the actual app rather than calling the modules directly, to confirm the main.js wiring and the worker path, and inspected the downloaded package.

Regression on the single-material path. Exported the built-in demo cube as 3MF and confirmed the output is structurally identical to before: one object with id 1, no basematerials, no config entry, same zip entries.

Viewport. Checked the toggle on and off, the 3D displacement preview, include-only mode with nothing selected (the worst case, where everything is masked), and a model with no colour data to confirm the toggle stays hidden and the mesh still renders teal. No console errors in any state.

Before, flat teal with no indication the model has parts:

image

After, each part shaded by its extruder with a legend:

image

Masked / include-only mode, part colours still readable under the mask tint:

Before applying the texture

image

After applying the texture

image

The exported file re-opened in the slicer, parts still on their original extruders:

image

Notes for reviewers

The trigger is deliberately conservative. Part data is only attached when the file has at least two parts and at least one extruder assignment or at least two distinct colours. A plain single body 3MF, or a multi-body one with no colour information, keeps exactly the previous merged behaviour. I did not want to silently change the output shape for files that gain nothing from it, though I am happy to widen this if you would prefer multi-body files to always round trip as separate objects.

STL export cannot carry this, since the format has no concept of colour. Only the 3MF path preserves it, and I noted that in the README.

Two prior PRs are adjacent but do not overlap in intent. #57 generates colour from the displacement heightmap rather than reading it from the file, and #63 preserved multiple bodies but not their colour or tool assignments. #57 does touch main.js, exporter.js and previewMaterial.js, so whichever lands second will need a merge, though the features themselves are complementary.

Unrelated observation, not addressed here. While testing I noticed resolveTJunctions gets extremely slow on this particular model at fine refine lengths, running for minutes on a roughly 68k triangle mesh. I confirmed with a control run that this is pre existing and unrelated to these changes, since it reproduces identically with the colour tracking disabled. I left it alone rather than widening the scope of this PR, but it seemed worth mentioning.

Conventions. Commit subject follows the Conventional Commits style used on main. Comments and prose use British spelling to match the surrounding code. New user-facing strings are added to en.js and es.js; the other locales fall back to English through the existing fallback in i18n.js, so nothing renders as a raw key.

A multi-colour 3MF (Bambu Studio / OrcaSlicer / PrusaSlicer) stores its
colour data in two places the importer discarded: <basematerials>
displaycolor in 3dmodel.model, and the per-part extruder map in
Metadata/model_settings.config. parse3MF flattened every object into a
single position array, so a textured export came back as one untinted
body and had to be repainted by hand.

Per-triangle palette slots are now carried from import to export, keyed
on face index rather than re-derived spatially:

- stlLoader: parse basematerials + model_settings.config into
  userData.materials {palette, triMaterial}. validateAndCleanGeometry
  compacts triMaterial in lockstep, since dropping degenerate triangles
  renumbers every face.
- exportPipeline: project slots onto the refined mesh through the
  existing faceParentId map, which export mode now also composes.
- decimation / meshRepair: carry slots through collapses and fan-splits.
  Decimation pins vertices shared by two materials — buildIndexed welds
  coincident vertices globally, so a collapse there would drag one
  filament's geometry into the other's and ragged the colour boundary.
- exporter: emit basematerials + one object per part + a components
  assembly + model_settings.config with extruders. Part names are XML
  escaped; they come from the imported file.

Also adds a Part Colours viewport toggle that shades each body by its
extruder (not by displaycolor — files routinely give several parts the
same swatch while printing them on different tools), with a legend
mapping colour to part name and tool. Masked and include-only regions
keep their part hue under the mask tint instead of going flat orange.

Models without colour data are unaffected: the exporter emits the same
single-object 3MF as before and the toggle stays hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-material 3MF: part colors and tool assignments not preserved through texturing

1 participant