Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com> Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
13 KiB
Precise Seam — High Level Design
Purpose and scope
Precise Seam places the seam where a helper volume intersects the external wall. The user attaches a mesh to an object as a Precise Seam modifier, and on every layer the seam placer reads the modifier's slice to decide where the seam of each external perimeter may, must or must not go. The same mesh keeps working after the model changes, so the seam does not have to be repainted after every design revision, and a swept helper body can guide the seam along any path.
The modifier is non-printing geometry. It does not take part in slicing, region assignment, filament selection or brim adhesion. It affects only seam placement, which runs during G-code export.
Volume types and priority
Precise Seam adds six ModelVolumeType values after SUPPORT_ENFORCER. The
strong types come first and the weak types follow. is_precise_seam(),
is_precise_seam_strong() and is_precise_seam_weak() are range checks that
depend on this order.
| Type | Group | Effect on the perimeter |
|---|---|---|
PRECISE_SEAM_CENTER |
strong | seam at the arc-length midpoint of the intersection |
PRECISE_SEAM_LEFT |
strong | seam at the first point of the intersection |
PRECISE_SEAM_RIGHT |
strong | seam at the last point of the intersection |
PRECISE_SEAM_ENFORCED |
weak | intersection marked as enforced |
PRECISE_SEAM_BLOCKED |
weak | intersection marked as blocked |
PRECISE_SEAM_NEUTRAL |
weak | intersection reset to neutral |
A strong modifier fixes one point. A weak modifier only changes the enforced/blocked type of seam candidates, and the configured seam position then chooses among them. First and last are taken along the perimeter made counter-clockwise seen from above. On an outer wall seen from outside, Left is the left end of the intersection. On the wall of a hole seen from inside the hole, the two ends are swapped.
The order of volumes in the object is the priority order, highest first.
ModelObject::sort_volumes() keeps every strong modifier before every weak one
and preserves the user's order within each group. The object list lets the user
drag a modifier only within its own group. A type change that crosses a group
boundary moves the volume to the end of its new group, where it has the lowest
priority. Strong modifiers are tried in this order, and the first one that
yields a seam on a perimeter wins. Weak modifiers are applied from the lowest
priority to the highest, so the highest one overwrites any overlapping zone.
Model storage and 3MF compatibility
Projects must stay readable by earlier releases, and the modifier must not
change a print there. Both 3MF writers therefore store a Precise Seam volume as
an ordinary parameter modifier: modifier_part in the Bambu-format part
subtype, and ParameterModifier together with the legacy modifier flag in
the Prusa-format volume metadata. The seam mode is written separately under
precise_seam_type, using the names from ModelVolume::type_to_string()
(precise_seam_center and so on).
On load, the mode applies after all other volume metadata, regardless of XML key order, and only when the base type is a modifier. Missing or unknown modes leave an ordinary modifier. Seam metadata on any other base type is ignored. Files that stored the seam mode directly as the volume type still load.
A Precise Seam volume keeps any per-volume settings it had as a part or
modifier, but they are inactive and the object list shows no settings item for
it. The writers prefix these keys with precise_seam_config:, so an earlier
reader drops them as unknown options. The volume therefore loads there as a
modifier without settings and has no effect on the print. The current reader
restores the keys only when the volume ends up as a Precise Seam type, so the
settings return when the user changes the type back. Configuration values are
XML-escaped in both writers, for every volume type.
Print invalidation
Print::apply() compares the Precise Seam volumes of each object by type, ID
and transformation. Adding, removing, moving, reordering or retyping one
cancels background processing and invalidates only psGCodeExport; the sliced
layers are kept. model_volume_list_update_supports_and_seams() then brings
the support and Precise Seam volumes of the print's model copy in line with the
new model in one pass. A volume may switch between the two families, since
neither affects slicing. A conversion to or from a part or ordinary modifier
changes the solid and modifier volume lists and reslices as before.
Modifier slices
SeamPlacer::init() collects the Precise Seam volumes of each object once:
strong ones in priority order and weak ones reversed. It slices each volume
separately with PrintObject::slice_single_volume(), which shares
slice_modifier_volumes() with support blockers and enforcers but does not
merge volumes, so each keeps its own priority. The result is cached per volume
and indexed by object layer; Layer::id() includes raft layers, which are
subtracted. Seam candidates are then gathered in parallel over the layers and
read the cache without locking.
Objects without Precise Seam volumes follow the unchanged seam placement path. For objects that have them, perimeter extraction also removes consecutive duplicate points and the repeated closing point of each extrusion loop. Zero-length edges at path junctions would otherwise prevent point insertion there. Distinct visits to one point of a self-touching contour are kept.
Finding the wall segment
The seam placer works on the external perimeter loops of each layer, both outer contours and holes, each made counter-clockwise. For every modifier polygon on the layer that overlaps the perimeter's bounding box, the region enclosed by the perimeter is clipped against the modifier polygon. The boundary of each intersection polygon alternates between runs that follow the perimeter and runs that follow the modifier outline. The wall segment is the longest continuous run of intersection vertices that lie on the perimeter, measured in vertices.
The fast path first finds an intersection vertex that exactly matches a perimeter vertex. It then walks forward and backward, expecting the adjacent perimeter vertex and falling back to projection when Clipper has merged or split collinear edges. A vertex counts as on the perimeter when its projection is within about 1.6 nm, which covers Clipper's rounding. If no vertex matches exactly, or every vertex lies on the perimeter, the general path projects all vertices. When every vertex is on the perimeter, the edge midpoints are checked instead: a modifier chord can join two perimeter vertices directly, and the chords split the vertex ring into runs. If no edge leaves the perimeter, the perimeter lies entirely inside the modifier.
Polygon::point_projection() optionally reports the edge that holds the
projection, and every point of the segment keeps the index of its perimeter
edge. New points are inserted on that edge. A point within 1 µm of an existing
vertex snaps to that vertex instead.
Strong modifiers
For a strong modifier, the target is the first point, the last point or the arc-length midpoint of the segment. The midpoint is projected back onto the original perimeter, because Clipper may have merged several perimeter edges into one segment edge. The target is inserted into the perimeter, and a helper point is inserted 1 µm before and after it. Strong modifiers are tried in priority order, the first valid intersection decides the seam, and weak modifiers are not processed for that perimeter.
When candidates are built, the inserted point is the only enforced candidate
and becomes the central enforcer; every other candidate is blocked. The seam
position modes then pick that point: Aligned and Aligned Back prefer the central
enforcer, while Back, Random and Nearest rank enforced candidates above blocked
ones. Alignment and random placement can still move the final position along an
edge. After alignment, restore_precise_seam_positions() writes the exact point
and its index back into every perimeter that has a strong seam. Inner walls take
their seam from the external seam as usual, including staggering.
Weak modifiers
Weak modifiers produce one segment per intersection polygon, so one modifier can mark several zones on one perimeter. All segment boundaries are inserted into the perimeter in order of decreasing arc length. Each insertion then leaves the indices of the pending, shorter ones unchanged; a point on the closing edge is appended rather than inserted at index zero. A helper point is added 1 µm outside each boundary. Random placement picks a position along the edge that follows a candidate. These helpers keep that edge 1 µm long at each boundary, so a zone cannot extend or intrude further than that. Boundaries that coincide share their helper points.
The zone types are then resolved in priority order, and the edges of enforced
zones are subdivided into steps of at most
SeamPlacer::enforcer_oversampling_distance (0.2 mm). The middle candidate of
the longest enforced patch is therefore close to the geometric middle of the
zone. That patch is measured in candidates, across the closing edge, regardless
of where the contour starts; the same rule applies to painted seams.
Candidates first receive their type from seam painting. The weak zones then overwrite it, lowest priority first. Blocked and Enforced zones therefore take precedence over painting, and Neutral clears painting inside its zone.
Unsupported geometry and warnings
Some modifier shapes cannot be resolved to one seam or one zone per crossing. They are detected cheaply and reported rather than guessed:
- A strong modifier that crosses a perimeter in more than one place uses only its first valid segment. The other crossings are ignored.
- A modifier that crosses the whole region enclosed by the perimeter is detected when the modifier outline minus that region leaves more than one piece, none of them a hole. Its intersection holds two wall runs, and only one of them is used.
- A modifier whose slice has a hole on a layer, found as a clockwise polygon in the flattened slice, is skipped on that layer. The flattened slice no longer records which hole belongs to which contour.
- A perimeter that lies entirely inside a modifier is ignored by that modifier.
The conditions are atomic flags shared by all layers and objects. After all
objects are processed, SeamPlacer::init() issues at most one non-critical
warning with the ID SlicingPreciseSeamWarning. The warning is a single line
that lists every cause found, because the export warnings dialog shows only the
first line of each warning. Repeated warning events replace this notification
instead of appending text to it.
User interface
- Add Precise Seam in the object menu creates a Center modifier from a
primitive or a loaded mesh. Text and SVG volumes cannot become Precise Seam
modifiers: the menu does not offer them, and
ObjectList::set_volume_type()refuses the change. - Change Type has a single Precise Seam entry. It converts other volumes to Center and keeps the mode of volumes that are already Precise Seam. The Precise Seam Type submenu appears only when every selected item is a Precise Seam volume, including settings rows that resolve to one. It sets the chosen mode on all selected volumes.
- Each mode has its own icon in the object list and its own color in the 3D view, at 60% opacity: warm oranges for the strong modes, and green, red and gray for Enforced, Blocked and Neutral.
- Object list drops map visible rows to volume indices while skipping hidden cut connectors, and they refresh the row-to-volume map of the object.
- Precise Seam volumes have no filament, block pasting into SLA, and are exposed
to Python plugins as
ModelVolumeTypevalues plus theis_precise_seam*()methods.
Implementation and verification
- PreciseSeam.cpp implements segment detection, point insertion, weak-zone resolution and position restoration. SeamPlacer.cpp integrates it into candidate gathering and issues the warning.
- Model.hpp defines the types and their order, PrintApply.cpp handles invalidation, and PrintObjectSlice.cpp slices the modifiers. bbs_3mf.cpp and 3mf.cpp store them.
- GUI_Factories.cpp and GUI_ObjectList.cpp provide the menus, type changes and ordering.
- Precise Seam tests cover the strong positions, including a midpoint on an existing vertex or the closing edge. They also cover shared and coincident weak boundaries, every warning, and the priority order.
- Seam placer tests cover
enforced-patch selection independent of the contour start, fully painted
contours, duplicate removal, and
Print::apply()synchronization through type changes and restored model snapshots. - 3MF tests cover the round trip of every mode and of inactive settings, attribute escaping, and which metadata combinations restore a seam mode. Plugin tests cover the Python bindings.