Skip to content

Preserve per-triangle painting through Boolean, merge and Simplify - #11444

Open
Cstm3DBldr wants to merge 2 commits into
bambulab:masterfrom
Cstm3DBldr:paint-hifi-pr
Open

Preserve per-triangle painting through Boolean, merge and Simplify#11444
Cstm3DBldr wants to merge 2 commits into
bambulab:masterfrom
Cstm3DBldr:paint-hifi-pr

Conversation

@Cstm3DBldr

@Cstm3DBldr Cstm3DBldr commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #11105, which brought painting through merge, split, merge-to-multipart and repair. This covers the remaining mesh-rebuilding operations: Boolean (union / intersection / difference, both the gizmo and the right-click menu), merge into single part, and Simplify.

Rebased onto current master.

Approach

PaintReproject.hpp already described this as the intended extension point:

"Boolean is not wired up yet, but reproject_paint_geometric is the intended extension point: pass the boolean result as the destination mesh and each participating operand mesh as a source (nearest-distance source selection or a merged source selector can be layered on later)."

reproject_paint_from_volumes() implements the nearest-distance source selection described there. Each operand is reprojected on its own through reproject_paint_geometric - a single source per call, so operands cannot overwrite one another where they overlap - and each destination face then takes the paint of the nearest operand that covers it. An operand only counts as covering a face when the closest point lies inside one of its own faces, which stops paint spreading past a part's outline where two parts share a plane; a tie there goes to the smaller part. A face no operand reached, such as surface newly cut by a boolean, takes the plain filament of its nearest operand. The header comment is updated accordingly.

Solid part color

A boolean or merge result is a single volume with a single extruder, so any area left unpainted silently takes that one color. TriangleSelector::fill_unpainted() bakes each operand's own filament into its unpainted areas while leaving painted detail untouched.

This also fixes color loss in merge-into-single-part: the per-face copy left the area around each brush stroke unpainted, and those faces fell back to the merged volume's extruder, so a part merged with a differently-colored one lost its color.

Simplify keeps its paint via ModelVolume::set_mesh_keep_paint() instead of set_mesh(). The clear_before_change_mesh() call that preceded it reset the paint on every volume of the object, which defeated the reprojection.

Second commit: self-intersecting parts

check_boolean_possible() runs CGAL, whose does_self_intersect() rejects a part outright, while the boolean that follows runs mcut, which copes with far messier meshes. Because a CGAL verdict skipped the attempt entirely, a model with even a couple of overlapping triangles never reached mcut - the operation fell through to the branch that merges the parts' meshes and reports a failure, so the user asked for a boolean and silently got the parts concatenated, overlap included.

Allowing SelfIntersect through lets mcut try. The call is already wrapped in try/catch and its result is only accepted when non-empty, so a mesh mcut cannot handle still lands in the same merge fallback and warning as before. This only turns former hard failures into successes. Note combine_mesh_fff() is shared with STL export, so a self-intersecting model now exports the boolean result rather than the merged fallback. Kept as a separate commit so it can be dropped independently.

Testing

Verified by decoding the per-triangle annotations out of saved 3MFs rather than by eye. Two parts on different filaments, each brush-painted, then each operation run and saved:

Operation Faces Unpainted Result
Union 48 0 correct per-part color, 16 faces retain brush detail
Intersection 22 0 0 mismatches against per-plane source ownership
Difference 36 0 cut walls take the kept part's filament
Merge into single part 24 0 exactly the source's 12 brush-painted faces
Merge -> Simplify 24 0 detail survives decimation
Merge -> Split to object 24 0
Merge -> Split -> Merge multipart 24 0
Union (part mode) + slice 48 0 colors reach the slicer

Cut and repair were re-checked for regressions and are unchanged; the surface a cut newly creates stays unpainted, as before.

Also exercised on a real 45,680-triangle sculpted model: unpainted faces dropped from 68.2% to 0.1%, brush detail rose from 704 to 1,063 faces as the boolean subdivided along existing paint boundaries, and the operation succeeded on a mesh that self-intersects.

Demos

Boolean Union (toolbar)
Boolean Union Toolbar

Boolean Intersection (toolbar)
Boolean Intersection Toolbar

Boolean Subtraction (toolbar)
Boolean Subtraction Toolbar

Right-click Boolean
Right Click Boolean

Simplify, high to low resolution
Simplify high-low res

Merge into single part - the operation this PR fixes for parts on different filaments (clip from #11105, showing the original merge preservation)
merge into single part paint preservation

Repair (unchanged by this PR, shown for continuity with #11105)
Repair

@Haidiye00 Haidiye00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approve

@Cstm3DBldr

Copy link
Copy Markdown
Contributor Author

How often do public releases get pushed and what is the deciding factor when to push?

@Cstm3DBldr

Copy link
Copy Markdown
Contributor Author

@Haidiye00 still waiting on the merge is there something i need to do to get this pushed in?

@lanewei120

Copy link
Copy Markdown
Collaborator

Hello @Cstm3DBldr

could you help to rebase to the newest code base?

sorry previously we missed this PR

@lanewei120
lanewei120 requested review from StoneLiBambu and removed request for FengMi-BamBu and zhimin-zeng-bambulab August 28, 2026 04:17
@Cstm3DBldr

Copy link
Copy Markdown
Contributor Author

@lanewei120
Sure i started to once i saw the new release. and got busy should be freed up in a day or so and ill get updated code uploaded.

I have a few tools that would be nice to have in the works as well mostly related to painting models. Ill Keep you posted when those are ready to merge and fully working.

Thanks for reaching out.

Boolean, merge-into-single-part and Simplify all rebuild a volume's mesh, and
until now the painted annotations did not survive the rebuild. PaintReproject
already carries paint through cutting and repair; this wires the remaining
mesh-rebuilding operations into it, following the extension point described in
PaintReproject.hpp ("pass the boolean result as the destination mesh and each
participating operand mesh as a source, nearest-distance source selection").

reproject_paint_from_volumes() reprojects one operand at a time through
reproject_paint_geometric - a single source per call, so operands cannot
overwrite one another where they overlap - and each destination face then takes
the paint of the nearest operand that covers it. An operand covers a face only
when the closest point lies inside one of its own faces, which keeps paint from
spreading past a part's outline where two parts share a plane; a tie there goes
to the smaller part. A face no operand reached, such as surface newly cut by a
boolean, takes the plain filament of its nearest operand.

A part's solid colour is carried as well as its brush strokes. A boolean or
merge result is a single volume with a single extruder, so an area left
unpainted would silently take that one colour. TriangleSelector::fill_unpainted()
bakes each operand's own filament into its unpainted areas while leaving painted
detail untouched. This also fixes colour loss in merge-into-single-part, where
the per-face copy left the area around each brush stroke unpainted and those
faces fell back to the merged volume's extruder.

Simplify keeps its paint by going through ModelVolume::set_mesh_keep_paint()
instead of set_mesh(); the clear_before_change_mesh() call that preceded it
reset the paint on every volume of the object, which defeated the reprojection.

Signed-off-by: Cstm3DBldr <34087122+Cstm3DBldr@users.noreply.github.com>
check_boolean_possible() runs CGAL, whose does_self_intersect() test rejects a
part outright, while the boolean that follows runs mcut, which copes with far
messier meshes. Because a CGAL verdict skipped the attempt entirely, a model
with even a couple of overlapping triangles never reached mcut: the operation
fell through to the branch below, which merges the parts' meshes and reports a
failure. The user asked for a boolean and silently got the parts concatenated,
overlap included, plus a warning.

Allowing SelfIntersect through lets mcut try. The call is already wrapped in
try/catch and its result is only accepted when non-empty, so a mesh mcut cannot
handle still lands in the same merge fallback and warning as before. This only
turns former hard failures into successes; nothing that already worked changes.

Note that combine_mesh_fff() is shared with STL export, so a self-intersecting
model now exports the boolean result rather than the merged fallback.

Signed-off-by: Cstm3DBldr <34087122+Cstm3DBldr@users.noreply.github.com>
@Cstm3DBldr Cstm3DBldr changed the title Preserve per-triangle painting through Boolean and Simplify (high-fidelity) Preserve per-triangle painting through Boolean, merge and Simplify Aug 29, 2026
@Cstm3DBldr

Copy link
Copy Markdown
Contributor Author

@lanewei120 Rebased onto the latest master — no conflicts, and it shows as mergeable now.

One thing I want to flag rather than let you find it in review: this is not the same patch
that was approved back in July. Rebasing onto 2.8.2 meant rewriting it against the new
PaintReproject module, and while I was testing the rebase I found the merge and Boolean
paths were still losing color in cases the original patch never covered. So the
implementation changed substantially and I think it deserves a fresh look rather than
carrying the old approval forward.

What it does now:

  • Boolean (union / subtract / intersect) keeps per-triangle paint. This covers both the
    synchronous and the asynchronous result paths — the async one is the default and was
    producing completely unpainted results.
  • Merge into single part copies the paint per face instead of reprojecting it, so the
    merged mesh is an exact copy of what the parts had.
  • Simplify no longer wipes paint. It was clearing all annotations before reprojecting.
  • A self-intersecting model is no longer blocked from Boolean outright; the tolerant path
    is allowed to attempt it, which real-world models need.

I checked the results by reading the per-triangle annotations back out of saved 3MFs rather
than judging by eye: union 48 faces / 0 unpainted, subtract 36 / 0, intersect 22 / 0, merge
24 / 0 with exactly the 12 painted faces the source had.

Separately, I opened #12088 for the SVG work I mentioned — reading CSS styling and clip
paths that the parser currently ignores, plus importing outline art as colored parts. It
touches different files and does not depend on this one, so the two can go in any order.

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.

3 participants