Files
OrcaSlicer/docs/HLSD/precise-seam.md
T

34 KiB

Precise Seam — High Level Design

Purpose and scope

Precise Seam lets a helper volume decide where the seam of an object goes. The user attaches a mesh to an object as a Precise Seam modifier. On every layer, the part of the external perimeter that lies inside the modifier's slice determines where the seam must, may or must not be placed. The helper is a persistent model object rather than paint on the surface, so it keeps working when the design changes. A body swept along a path on the surface can guide the seam along any trajectory.

The modifier is non-printing geometry. It takes no part in object slicing, region assignment, filament selection or brim adhesion, and it affects only seam placement during G-code export. Objects without Precise Seam volumes follow the regular seam placement unchanged.

Precise Seam does not replace the seam placer. It feeds it: a modifier inserts the points it needs into the perimeter and changes the enforced/blocked type of seam candidates, the same typing mechanism as seam painting, and the configured seam position then chooses among them.

Modifier types

Precise Seam adds six ModelVolumeType values after SUPPORT_ENFORCER, strong types first and weak types after them. is_precise_seam(), is_precise_seam_strong() and is_precise_seam_weak() are range checks that depend on this order.

Type Group Effect on an intersected perimeter
PRECISE_SEAM_CENTER strong seam at the midpoint, by arc length, 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 a single point. The perimeter gets exactly one enforced seam candidate there, and every other candidate is blocked. A weak modifier retypes, and where needed adds, the candidates inside its intersection, like painting does.

An intersection is a continuous part of the external perimeter's centerline that lies inside the modifier's slice on that layer. It is a portion of the perimeter, never a chord through the object. The centerline lies half an extrusion width inside the model surface and depends on print settings, so a modifier must reach clearly past the surface to cross it unambiguously.

Terms

  • Segment: an intersection as the code represents it (PerimeterSegment). User-facing texts call it an intersection.
  • Fragment: a piece of the perimeter returned by clipping, before it is tied to the source contour.
  • Interval: the bound part of one source edge, given by the edge index and a parameter range on that edge.
  • Zone: a weak segment with its type (Enforced, Blocked or Neutral).
  • Boundary: an end of a zone, inserted into the perimeter polygon.
  • Candidate: a seam candidate of the seam placer, built from the points of the processed perimeter polygon (painted enforcers may add more).

First and last are taken along the perimeter oriented counter-clockwise as seen from above. On an outer wall seen from outside, Left is therefore the left end of the intersection. On the wall of a hole seen from inside the hole, the two ends are swapped. Mirroring an object does not mirror the mode: perimeters stay counter-clockwise, so Left remains the left end seen from outside, and the seam moves to the other end of the modifier instead of following the mirrored model.

Priority

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 the group boundary moves the volume to the end of its new group, with the lowest priority there.

  • Strong: modifiers are tried in priority order on each perimeter. The first one that yields a usable segment decides the seam. Within that modifier the longest segment wins; lengths are never compared across modifiers. Once a strong point is placed, no later strong modifier and no weak modifier is processed for that perimeter.
  • Weak: every weak modifier applies. They are applied from the lowest priority to the highest, so the highest one overwrites overlapping zones. A Blocked modifier that fully contains a perimeter is the exception: it is skipped there (see Full containment).

A strong modifier without a usable segment, even one whose fragments were all discarded, passes the turn to the next one.

Data flow

  1. Invalidation. Print::apply() treats a change of Precise Seam volumes as a change of seam placement and invalidates G-code export; the object is not resliced (see Print invalidation).
  2. Modifier slices. SeamPlacer::init() collects each object's Precise Seam volumes once, slices every volume separately and caches its regions with their bounding boxes.
  3. Perimeters. Seam candidates are gathered in parallel over the layers. For objects with Precise Seam volumes, each external perimeter polygon is normalized and prepared once for all modifiers.
  4. Extraction. For each modifier, the perimeter is clipped against the modifier's regions on that layer. The clipped fragments are bound back to the source edges of the perimeter and assembled into segments.
  5. Strong, then weak. Strong modifiers try to insert one seam point into the perimeter polygon. If none succeeds, weak modifiers insert their zone boundaries and subdivide enforced edges.
  6. Candidates. The seam placer builds candidates from the modified polygon. Painting assigns types first, weak zones overwrite them, and a strong point makes its candidate the only enforced one.
  7. Selection and restoration. The configured seam position chooses the seams and aligns them. Afterwards the exact strong points are restored.
  8. Warnings. After all objects are processed, SeamPlacer::init() prepares one combined warning text if any problem was found; G-code export issues it.

Modifier slices

init_precise_seam_data() collects the Precise Seam volumes of each object: strong ones in priority order and weak ones in reverse, so that weak zones can be applied with last-write-wins. Each volume is sliced separately with PrintObject::slice_single_volume_regions(), at the object's layer heights and with the same centered transformation as the object. The slices keep every region's outer contour together with its holes as an ExPolygon. Volumes are not merged, so each keeps its own priority, and a modifier may have several regions on one layer.

prepare_modifier_slices() moves the slices into ModifierRegionsCache, pairing each region with the bounding box of its exterior. Empty layers keep their slots, so the cache is indexed by object layer; Layer::id() includes raft layers, which are subtracted. The cache is filled before candidates are gathered and is only read afterwards, shared by both modifier kinds and all worker threads without locking.

Perimeter preparation

The seam placer works on external perimeter loops, including the walls of holes. For objects with Precise Seam volumes, consecutive duplicate points and the repeated closing point of each extrusion loop are removed: adjacent extrusion paths share endpoints, and the resulting zero-length edges would prevent point insertion at their junctions. Distinct visits to one point of a self-touching contour are kept. Objects without Precise Seam volumes keep their original points, so ordinary seam candidates are unaffected.

Each polygon is made counter-clockwise. A single PreparedPerimeter is then built for all modifiers of that perimeter. It holds a validity check (at least three points, no consecutive or closing duplicates), the bounding box, and the clipping line: the polygon as an open polyline with its first point repeated at the end. The preparation borrows the polygon and is used only while the polygon is unchanged: strong processing returns immediately after inserting its point, and weak processing collects all segments before it inserts anything. An invalid perimeter receives no Precise Seam processing.

Segment extraction

extract_perimeter_segments() turns one modifier's regions on one layer into segments of the perimeter, each with its geometry and its position on the source contour. Both modifier kinds consume these segments; the extractor is told the modifier type so that it prepares only the data that type needs.

Clipping

Regions whose bounding box does not overlap the perimeter's are skipped. The clipping line is intersected with each remaining region by intersection_pl(), which clips an open path against an ExPolygon with its holes attached, using the nonzero rule. Clipping an open line yields only pieces of the perimeter, so a modifier crossing the whole object produces two separate pieces rather than a chord through the body. Holes in a modifier and several regions of one modifier simply produce more pieces. The line is cut at vertex zero, so a piece crossing that vertex arrives as two fragments. A border that only touches the line can come back as a single point; such fragments carry no coverage and are dropped before binding.

Binding fragments to source edges

Clipper returns coordinates only. Insertion needs the source edge of every point, and coordinates alone are ambiguous where a contour visits the same point twice. Each fragment is therefore bound to the source edges it covers, producing intervals: an edge index with a parameter range on that edge.

  • Exact path. For fragments with interior points, the second point is used as an anchor that must equal a source vertex exactly. Clipping keeps the vertices of an open path unchanged, including collinear ones. The following points must match successive source vertices in either direction; later occurrences of the anchor are tried if a sequence does not match. Only the two end cuts are projected onto their edges.
  • Projection path. Two-point fragments, and fragments the exact path cannot match, are bound by projection. The first source edge that holds both points of the first pair, with distinct parameters, establishes the edge and direction. Every following pair must continue on the same edge or cross to the neighboring edge at their actual shared vertex, in the same direction. A pair continuing on the same edge reuses the previous pair's parameter for their shared point, so the two projections of one point cannot differ.
  • Failure. A fragment that cannot be bound continuously is rolled back and discarded. Earlier fragments and other fragments are unaffected. The failure is counted, logged and reported to the user (see Diagnostics).

Two rare rounding cases are handled only after both paths have failed, so the normal path never pays for them:

  • Cut beside a vertex. When a modifier boundary crosses within about one coordinate unit of a source vertex, Clipper can place the cut at the vertex's height but a few units beside it. The end pair then collapses to the vertex's parameter or misses both neighboring edges. An end cut closer than the snapping radius to a vertex of the fragment's own chain is snapped to that vertex: either its neighbor in the fragment (the cut is a rounded copy of it and is dropped) or a vertex that shares a source edge with that neighbor. The neighbor wins whenever it is within the radius. Ends that are themselves source vertices and ambiguous choices are left unchanged. Binding is then retried once with the same strict rules, so a wrong candidate can only fail again.
  • Contact. A fragment that still fails but is shorter than the snapping radius is accepted as a contact and binds nothing. Insertion would collapse it onto one point anyway.

Both outcomes are recoveries, not failures: they show no user warning but leave a log marker.

Assembling segments

The intervals are sorted by edge and parameter. Intervals on the same occurrence of an edge are united when they overlap or meet, by parameter or at the same integer point; equal coordinates on different edges are never united. A parameter of 1 is stored as parameter 0 of the next edge, so intervals on adjacent edges meet exactly at their shared vertex. Consecutive intervals that meet form one PerimeterSegment, and the last segment is joined with the first when they meet at vertex zero, undoing the artificial cut of the clipping line.

Each segment keeps its polyline, the source edge of every polyline edge, and its begin and end positions on the source contour.

Full containment

A modifier that covers the whole perimeter has no boundaries on it. The policy follows seam painting, where painting a whole perimeter green is a meaningful choice and forbidding the seam all round is not:

  • Seam Enforced types the whole perimeter, like a perimeter painted green all round, with subdivision applied as described under Weak modifiers.
  • Seam Neutral types the whole perimeter Neutral, like an unmarked perimeter, clearing painting and lower zones.
  • Seam Blocked is skipped for the perimeter, with the full-containment warning. The seam cannot avoid the whole perimeter, so the modifier does not override anything below it: lower zones and painting stay in effect.
  • Seam Center, Left and Right are skipped with the same warning: there is no intersection to place the point on.

Enforced and Neutral take part in the usual priority order (see Weak modifiers).

The perimeter is fully contained when the united intervals cover every source edge from parameter 0 to 1. A modifier boundary that merely touches the perimeter counts as well:

  • At a vertex or on an axis-aligned edge, clipping splits the line exactly at the touch, the pieces meet at one point, and the coverage is complete.
  • On an inclined edge the touching point is usually not representable on the integer grid. The boundary pokes a few units across and leaves a real gap, so a single segment covers everything except that gap.

Weak insertion would collapse such a segment's boundaries onto one vertex and turn the intended zone into a single candidate, and strong would put the seam at the touch. A single segment is therefore also full containment in the cases where insertion collapses it, exactly up to edges shorter than 2 µm:

  • the uncovered length from its end to its begin is below 1 µm, or
  • the gap spans one vertex, or starts at a vertex and ends on the next edge, and both ends lie within 1 µm of the vertex that ends the first gap edge, since each end then snaps onto it from its own edge.

A cheap filter runs first: both cases bring the segment's ends within 2 µm of each other.

Strong modifiers

For a strong modifier, the extractor prepares each segment's target point before anything is inserted, together with the source edge it lies on:

  • Left: the segment's first point.
  • Right: the segment's last point.
  • Center: the point at half the segment's arc length.

Arc length is the sum of Euclidean edge lengths, not the chord or a vertex count.

insert_strong_seam_point() selects the longest segment of the first modifier that has one. Exactly equal lengths are resolved by the prepared target points: greater bed Y first, then smaller X; a complete tie keeps the first segment. Slice coordinates already include instance rotation and have the bed axes; centering and XY translation do not change this order. Nearly equal lengths are not treated as equal, so exact ties occur mainly on axis-aligned geometry. Geometrically equal segments, such as a symmetric modifier crossing both faces of a thin wall, differ only by rounding noise that varies between layers, so the chosen face may alternate. This is accepted deliberately: such a modifier is ambiguous by itself: more than one segment raises the "multiple intersections" warning. The user should make the modifier cross the perimeter once.

The selected point is inserted on its source edge. A point within 1 µm of an existing vertex is snapped to that vertex. Helper points are added 1 µm on both sides of it, except on an adjacent edge shorter than 2 µm, which already bounds the distance.

When the candidates are built, the candidate at the inserted point is the only enforced one and becomes the central enforcer; every other candidate is blocked. Every seam position mode therefore selects it. Alignment and random placement can still move the final position, so after alignment restore_precise_seam_positions() writes the exact point and its index back into every perimeter that has a strong seam.

Weak modifiers

collect_weak_modifier_segments() extracts the segments of every weak modifier before the polygon is modified, so all positions refer to the same contour. Each segment becomes a zone with a type and two boundaries, kept in application order, lowest priority first. Full containment of an Enforced or Neutral modifier becomes a whole-perimeter zone at its place in that order: it has no boundaries and takes part in no insertion or helper step below. The boundaries carry their positions on the source contour; these remain as provenance after insertion and are not indices into the modified polygon.

prepare_weak_modifier_segments() then changes the polygon:

  1. Boundary insertion. Insertion events are sorted by decreasing source edge and parameter, and the polygon is modified from its end towards its start. A pending boundary's source index therefore stays valid. Vertex zero has the canonical position (0, 0) and is processed last, and a point on the closing edge is appended rather than inserted at index zero. A boundary within 1 µm of either endpoint of its current edge, an original vertex or a boundary inserted earlier, is snapped to that point, so coincident boundaries share a vertex. A zone narrower than 1 µm collapses into a single vertex.
  2. Helper points. A helper point is added 1 µm outside every boundary, unless the edge there is shorter than 2 µm, which already bounds it. The helpers keep the edges at a boundary short, so a seam placed along such an edge stays close to the boundary. Coincident boundaries share their helpers.
  3. Enforced subdivision. Zone types are resolved for the polygon's edges in priority order. The edges of a zone are those from its left boundary up to, but not including, its right boundary; a whole-perimeter zone types every edge. Enforced edges longer than SeamPlacer::enforcer_oversampling_distance (0.2 mm) are subdivided into steps of at most that length; shorter edges and existing vertices are kept. The regular seam placer then chooses the seam as for painted seams.

When candidates are built, painting assigns their types first. apply_weak_modifiers_to_perimeter() then overwrites the types of the candidates between the boundaries of each zone, both boundaries included, lowest priority first; a whole-perimeter zone types every candidate. Blocked and Enforced zones therefore take precedence over painting, and Neutral clears painting inside its zone.

Numeric tolerances

Coordinates are integers in scaled units: 1 nm by default, and 10 nm when a bed larger than 2147 mm switches SCALING_FACTOR. Both Precise Seam tolerances are deliberately defined in units rather than physical distances. Clipper truncates cuts to whole units at any scale, so the on-edge tolerance must follow the unit; the snapping radius scales with it to keep its margin over single-precision candidate coordinates, which are coarser on large beds. Distances quoted in this document in nanometers and micrometers assume the default unit; on large printers they are ten times larger. The enforced subdivision step is a physical distance and stays 0.2 mm.

Value Role
MACHINE_PRECISION_SQUARED (2.5 units², about 1.6 nm) A point lies on an edge if it is this close. It absorbs Clipper's truncation of cuts to whole units (under √2 units from the edge) and never bridges a real gap: a one-unit uncovered gap stays a gap.
TOLERANCE_LINEAR (1000 units, 1 µm) Insertion snaps points this close to an existing vertex, and helper points are placed this far from boundaries. The same radius bounds the rounding fallback, contacts and the sub-micron full-containment rule, so those decisions match what insertion would produce anyway.
enforcer_oversampling_distance (0.2 mm) Maximum step of enforced subdivision.

Raising the on-edge tolerance would not help with cuts beside a vertex: more points past a vertex would be clamped to its parameter and collapse. Lowering it would reject ordinary rounded cuts. The snapping radius is kept far above clipping precision for robustness: seam candidates hold single-precision coordinates, whose step is about 8 to 15 nm at typical object coordinates (about 0.25 µm 3 m from the object's centre, on large beds only), and weak boundaries and the strong point are located among the candidates by those coordinates, so distinct points must stay clearly distinct. 1 µm is also far below printing precision.

Diagnostics and warnings

One PreciseSeamWarnings instance is shared by all objects and layers of a SeamPlacer::init() call. After all objects are processed, SeamPlacer::init() prepares at most one warning text, available through precise_seam_warning(). G-code export issues it as one non-critical warning with the ID SlicingPreciseSeamWarning. It is a single line, "Precise Seam: . Seam placement may differ from expected.", because the export warnings dialog shows only the first line of each warning. Repeated warning events replace the notification instead of appending to it. Except for the "had no effect" cause, the causes name the modifier types involved, as the menu names them, in menu order and each type once, for example "(Seam Left, Seam Enforced)". The causes are:

  • failed to process some intersections (types): at least one fragment was discarded by binding. Other segments remain usable.

  • multiple intersections with a perimeter, only one was used (types): a Seam Center, Left or Right modifier had more than one segment on a perimeter (see Strong modifiers).

  • a perimeter is fully inside a modifier, the modifier was not applied to it (types): a Seam Center, Left, Right or Blocked modifier was skipped for a perimeter (see Full containment).

  • modifier "" of "