Skip to content

Read an SVG's styling and clip paths, and import outline art as colored parts - #12088

Open
Cstm3DBldr wants to merge 1 commit into
bambulab:masterfrom
Cstm3DBldr:svg-colour-import
Open

Read an SVG's styling and clip paths, and import outline art as colored parts#12088
Cstm3DBldr wants to merge 1 commit into
bambulab:masterfrom
Cstm3DBldr:svg-colour-import

Conversation

@Cstm3DBldr

@Cstm3DBldr Cstm3DBldr commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Closes #8044.

Two related changes to SVG import: the parser is taught to read styling and clip paths it currently ignores, and on top of that a new import builds a part for every color an outline drawing is made of.

The first half is a fix to SVG import as it stands today. The second is additive and can be dropped if it is not wanted.

Reading what the file says

NanoSVG reads only presentation attributes. It sees neither the styling a drawing program keeps in a <style> block nor the clip paths that trim the artwork, and it has no notion of <clipPath> at all, so it draws the clip shapes as though they were part of the drawing.

For a file exported with "style elements" - the default in common drawing programs - the result is that every shape arrives the same default color, clipped artwork arrives whole and spills past the edge it was meant to stop at, and the clip shapes appear in the model as stray geometry.

prepare_svg() folds both into the elements before parsing:

  • class rules become plain fill and stroke attributes, leaving alone anything the element already states itself. Several classes on one element are applied in order, the later winning, as CSS does.
  • the elements a clip applies to - named directly or through a class - are marked, so their shapes can be recognised once parsed. The parser keeps an element's id and hands a group's id down to the shapes inside it, which is what makes a clipped group recognisable.
  • the clip shapes are copied into the drawing rather than read on their own. This matters: the parser sizes a document from the bounds of what it contains, so a clip path read separately is measured against itself and no longer lines up with what it trims.

collect_clip_regions() gathers the clip regions once the document is parsed, and create_shape_with_ids() trims the shapes each clip applies to. Both are shared, so importing an SVG through the existing gizmo now keeps the colors the file gives it, honours its clip paths, and no longer draws the clip shapes.

Nothing about sizing changes, and a file that carries neither a <style> block nor a clip path is returned untouched.

Before - the same file on current master. Every shape arrives one color, the clip is ignored, and the clip circles are drawn as artwork:

svg stock import

After:

svg Fixed import

Importing outline art as colored parts

File > Import > Import SVG as color regions

Outline art carries its meaning in the areas its lines enclose, which the file never states - only the lines are drawn. Plain import draws what the file says to draw, so such a file arrives as a set of thin ribbons rather than the shapes a reader sees.

This takes each closed path's enclosed area instead, and:

  • resolves overlaps the way the drawing itself does - a shape keeps only what nothing painted after it covers - so the regions do not overlap and cannot fight over the same space
  • extrudes each region separately, since a single merged outline would be triangulated without regard to where one color meets the next, leaving facets straddling two regions
  • gives each region a filament matching the color it was drawn in, adding the colors the project does not have when asked to
  • centres the object on the plate being worked on

Colors the project does not have

Matching regions to whatever filaments happen to be loaded falls apart when there are not enough of them: a three-color drawing imported into a project with one filament would arrive as a single part in that color, with nothing on screen to say why. That is the situation #8044 describes, so the import compares the colors the artwork is drawn in against the loaded filaments and offers to add the ones that are missing:

This SVG is drawn in 3 colors and the project has 1 filament.

The 2 missing colors can be added to the project, or every region can be matched to the filaments already loaded.

Adding goes through Sidebar::add_custom_filament(), the same path as the + button in the filament panel, so they are ordinary project filaments carrying the SVG's own RGB. Matching what is already loaded stays available, and the question is skipped when every color the drawing uses is already there - so a project set up with the right filaments imports without being asked anything.

Artwork resolves to only as many colors as there are filaments to print it in, which for a drawing of any size is far fewer than it has regions - the logo below is 136 regions in 3 colors. Since handling 136 parts to change 3 colors is nobody's idea of convenient, the import asks:

This SVG is drawn as 136 regions in 3 colors.
They can be imported as one part per color, or kept as one part per region.

One part per color merges the regions printing in each filament, so recoloring is a click per color. The regions of a color are disjoint islands, so this is only their meshes concatenated - nothing overlaps and no geometry has to be resolved - and Split to objects brings the individual regions back. The question is skipped when every region is already its own color, there being nothing to choose.

One part per color - the logo arrives as three parts, one per filament:

svg Color import grouped

One part per region - the same import keeping all 136:

svg Color import regions

Split to objects, recovering the individual regions from a merged color:

split to objects

It is a separate action rather than a change to SVG import, because the two produce different things: the gizmo makes one editable volume, this makes a part per color. Anyone importing an SVG today gets exactly what they got before.

Testing

Built on Windows and exercised on real artwork - a logo of 211 shapes using CSS classes, two clip paths on groups, and overlapping fills throughout. Results were checked by reading the per-triangle data back out of the saved 3MF rather than by eye:

File Result
Logo with CSS classes + 2 clip paths 196 closed paths resolve to 136 non-overlapping regions, split 66 / 69 / 1 across the three colors it is drawn in; clip regions measure exactly the radii the file gives them
Same logo, one part per color 3 parts on filaments 1 / 2 / 3, the artwork unchanged
Same logo, one filament loaded offered the 2 missing colors; added them and the artwork arrives on its own three filaments rather than collapsed onto one
Same file through the existing SVG gizmo colors and clipping now honoured; clip shapes no longer drawn
Outline art, colors declared per path 18 regions, 4 and 14 across two filaments
Same art drawn entirely in black 18 regions, one filament
Nested loops, four colors 4 concentric regions, each matched to its nearest filament

Plain outline artwork, where the drawing states only the lines and not the areas they enclose:

svg outline import

Sliced, showing the regions resolve to the right filaments:

Slice After Import (small)

Notes

Curves are flattened to the same 0.1 mm chord tolerance the SVG gizmo already uses.

Not handled, and left for later: clipPathUnits="objectBoundingBox", a transform on a clipped group, and CSS selectors other than class rules. Gradients have no single color to print and are skipped, as before.

@Cstm3DBldr Cstm3DBldr changed the title Read an SVG's styling and clip paths, and import outline art as coloured parts Read an SVG's styling and clip paths, and import outline art as colored parts Aug 30, 2026
@Shane-Bambu
Shane-Bambu removed their request for review August 31, 2026 02:11
…ed parts

The SVG parser reads only presentation attributes, so it sees neither the styling a
drawing program keeps in a <style> block nor the clip paths that trim the artwork. A file
exported with "style elements", which is the default in common drawing programs, therefore
arrives with every shape the same default color, and clipped artwork arrives whole with
the clip shapes themselves drawn as if they were part of the drawing.

prepare_svg() folds both into the elements before parsing: class rules become plain fill
and stroke attributes, and the elements a clip applies to are marked so their shapes can be
recognised afterwards. The clip shapes are copied into the drawing rather than read on
their own, because the parser sizes a document from the bounds of what it contains - read
separately a clip path is measured against itself and no longer lines up with what it
trims. collect_clip_regions() gathers them once parsed and create_shape_with_ids() trims
the shapes each one applies to.

This is shared, so importing an SVG through the existing gizmo now keeps the colors the
file gives it, honours its clip paths, and no longer draws the clip shapes.

On top of that, "Import SVG as color regions" builds a part for every area the artwork is
drawn as. Outline art carries its meaning in the areas its lines enclose, which the file
never states: only the lines are drawn. Each closed path's enclosed area is taken, overlaps
are resolved the way the drawing itself resolves them - a shape keeps only what nothing
painted after it covers - and every region is extruded separately, since a merged outline
would be triangulated without regard to where one color meets the next. Each region is
then assigned the loaded filament nearest the color it was drawn in, and the object is
centred on the plate being worked on.

Artwork resolves to only as many colors as there are filaments to print it in, far fewer
than it has regions, so the import offers to merge the regions of each color into one
part - recoloring is then a click per color rather than per region. Those regions are
disjoint islands, so merging them is only a concatenation, and Split to objects
recovers them. The choice is skipped when every region is already its own color.

Colors the project does not have are offered as additions rather than silently
collapsed. With one filament loaded a three-color drawing previously imported as a
single part, because every region matched the only filament there was - which is
precisely the case issue bambulab#8044 describes. The import now compares each color the
artwork uses against the loaded filaments and, when some are missing, offers to add
them through Sidebar::add_custom_filament(), the same path as the + button; matching
what is already loaded stays available and the question is skipped when nothing is
missing.

Signed-off-by: Cstm3DBldr <34087122+Cstm3DBldr@users.noreply.github.com>
@Cstm3DBldr

Copy link
Copy Markdown
Contributor Author

Pushed an update.

Adding a color region import turned out to have a hole in it: the regions were
matched to whatever filaments the project already had, so importing a three-color
drawing into a project with one filament loaded produced a single part in that one
color, with nothing to explain why. That is exactly the situation #8044 describes -
someone with a multi-color SVG who wants the colors separated - so shipping it that
way would have missed the point of the issue.

The import now compares each color the artwork is drawn in against the loaded
filaments, and when some are missing it offers to add them:

This SVG is drawn in 3 colors and the project has 1 filament.
The 2 missing colors can be added to the project, or every region can be
matched to the filaments already loaded.

[Add 2 colors]  [Match loaded filaments]  [Cancel]

Adding goes through Sidebar::add_custom_filament(), the same path as the + button in
the filament panel, so they are ordinary project filaments carrying the SVG's own
RGB. Matching what is already loaded stays available, and the question is skipped
entirely when every color the drawing uses is already there - so a project set up
with the right filaments imports exactly as before.

Also went back over the diff for anything a reviewer should not have to read: a
header include the change no longer needed, a fallback branch that could not be
reached, and a struct field written and read in the same breath. About 30 lines
came out and a few comments were shortened; no behavior changed.

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-color image import

1 participant