feat(3mf): preserve per-part colours and extruder assignments through texturing - #111
Open
jcarranz97 wants to merge 1 commit into
Open
feat(3mf): preserve per-part colours and extruder assignments through texturing#111jcarranz97 wants to merge 1 commit into
jcarranz97 wants to merge 1 commit into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 adisplaycolorper body, with each<object>pointing into it viapidandpindex. The part to extruder mapping, which is what actually matters for a multi-tool print, lives inMetadata/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.configis not part of the spec, it is treated as optional andbasematerialsacts 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:
faceParentIdmap, so slots are projected through it. Export mode now composes that map too, since previously only bake mode needed it.Decimation needed one extra guard.
buildIndexedwelds 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 aMetadata/model_settings.configcarrying 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">, nobasematerials, 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 theirdisplaycolor, 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
partColorvertex 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
basematerialsresource, three part objects with correctpidandpindex, the components assembly and amodel_settings.configwith 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.jswiring 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:
After, each part shaded by its extruder with a legend:
Masked / include-only mode, part colours still readable under the mask tint:
Before applying the texture
After applying the texture
The exported file re-opened in the slicer, parts still on their original extruders:
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.jsandpreviewMaterial.js, so whichever lands second will need a merge, though the features themselves are complementary.Unrelated observation, not addressed here. While testing I noticed
resolveTJunctionsgets 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.jsandes.js; the other locales fall back to English through the existing fallback ini18n.js, so nothing renders as a raw key.