mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-04 22:31:02 +00:00
Precise Seam: remove known limitations and rework perimeter intersection (#16072)
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
This commit is contained in:
co-authored by
Ian Bassi
parent
73a4ff9b16
commit
2b4a1d88ec
+529
-183
@@ -2,238 +2,584 @@
|
||||
|
||||
## 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.
|
||||
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 does not take part in slicing, region
|
||||
assignment, filament selection or brim adhesion. It affects only seam
|
||||
placement, which runs during G-code export.
|
||||
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.
|
||||
|
||||
## Volume types and priority
|
||||
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.
|
||||
|
||||
Precise Seam adds six `ModelVolumeType` values after `SUPPORT_ENFORCER`. The
|
||||
strong types come first and the weak types follow. `is_precise_seam()`,
|
||||
## 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 the perimeter |
|
||||
| Type | Group | Effect on an intersected perimeter |
|
||||
| --- | --- | --- |
|
||||
| `PRECISE_SEAM_CENTER` | strong | seam at the arc-length midpoint of the intersection |
|
||||
| `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 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.
|
||||
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 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.
|
||||
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.
|
||||
|
||||
## Model storage and 3MF compatibility
|
||||
- **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](#full-containment)).
|
||||
|
||||
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
|
||||
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](#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](#diagnostics-and-warnings)).
|
||||
|
||||
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](#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](#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: <causes>. 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](#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](#full-containment)).
|
||||
- **modifier "<name>" of "<object>" had no effect on the seam (it might not reach
|
||||
the centerline of the printed perimeter):** a modifier was evaluated on at
|
||||
least one perimeter and never gave a segment, full containment or a discarded
|
||||
fragment. Only the first such modifier in print and volume order is named,
|
||||
followed by "(N in total)" when there are several.
|
||||
|
||||
Only the effect is certain, so the cause is given as a hint. A modifier is
|
||||
evaluated only when its turn comes: on a perimeter where a higher strong
|
||||
modifier placed the seam, lower strong and all weak modifiers are not
|
||||
evaluated. A modifier that was never evaluated is not reported, since nothing
|
||||
is known about it. A point contact gives no segment and does not count as
|
||||
reaching the perimeter.
|
||||
|
||||
The log records the following diagnostic markers:
|
||||
|
||||
- `[PreciseSeamIntersectionFailed]` for a discarded fragment, with object,
|
||||
modifier, layer, height, fragment and failing pair, the failure reason and
|
||||
point counts.
|
||||
- `[PreciseSeamFragmentRecovered]` for a recovery, with `outcome=bound` or
|
||||
`outcome=contact`, the same location fields and the original failure reason.
|
||||
- `[PreciseSeamNoEffect]` for every modifier of the "had no effect" cause, with
|
||||
the object and modifier names. Unlike the user warning, the log lists all of
|
||||
them.
|
||||
|
||||
Failures and recoveries are counted separately. The first 10 of each per
|
||||
`init()` call are logged in detail, in parallel processing order; if a limit is
|
||||
exceeded, one summary marker reports the total and the number omitted.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- **The modifier must reach the perimeter centerline.** Contacts are taken as
|
||||
clipping returns them, without offsets or tangency rules, so boundaries that
|
||||
only graze the centerline are the user's responsibility. Several near-touches
|
||||
on inclined edges can leave several segments separated by gaps of a few units;
|
||||
their zones then cover nearly the whole perimeter instead of being treated as
|
||||
full containment.
|
||||
- **Self-touching perimeters.** Extraction keeps distinct visits of one
|
||||
coordinate apart through its source-edge bindings, but the consumers locate
|
||||
inserted points by coordinates. A weak zone is typed and subdivided from the
|
||||
first vertex with its boundary coordinate, while boundary helpers are added at
|
||||
every such vertex. A strong point marks every candidate at its coordinate as
|
||||
enforced, and the last one is restored after alignment. If a boundary or a
|
||||
strong point falls exactly on a repeated coordinate, a zone may therefore start
|
||||
from another visit, or the seam may start at another visit of the same point.
|
||||
Carrying visit identity through insertion, refinement, candidates and
|
||||
restoration would touch the whole pipeline, so it is not done for this rare
|
||||
geometry. Overlapping source visits are likewise outside the binding contract.
|
||||
|
||||
## Integration with the application
|
||||
|
||||
### Other seam settings
|
||||
|
||||
- Precise Seam takes part only in outer and hole perimeter seam placement. In
|
||||
spiral vase mode the seam placer is not used for perimeters, so the modifiers
|
||||
have no effect.
|
||||
- Scarf seams, the seam gap and wiping start from the chosen point exactly as
|
||||
they would from an ordinary seam.
|
||||
- Seam painting acts only from model parts, the volumes the seam gizmo shows and
|
||||
edits, and from negative volumes. Painting retained on a volume after a change
|
||||
from part to a Precise Seam, ordinary or support modifier is ignored. A type
|
||||
change back to a model part reactivates any retained painting.
|
||||
Negative volumes keep it on purpose: painting a
|
||||
part and turning it into a negative volume is the only way to paint the wall
|
||||
of the hole it cuts. That painting still affects the seam but is invisible in
|
||||
the gizmo and cannot be edited there; this is known technical debt.
|
||||
If painting them is ever made editable, G-code invalidation must track it too:
|
||||
`model_custom_seam_data_changed()` checks model parts only.
|
||||
|
||||
### Model storage and 3MF compatibility
|
||||
|
||||
Projects must stay readable by earlier releases, and a Precise Seam volume must
|
||||
not change a print there. Both 3MF writers therefore store it 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.
|
||||
On load, the mode is applied after all other volume metadata, regardless of XML
|
||||
key order, and only when the base type is a modifier. A missing or unknown mode
|
||||
leaves an ordinary modifier, and seam metadata on any other base type is
|
||||
ignored. Files that stored the seam mode directly as the volume type still load.
|
||||
A project saved again by an earlier release loses the seam mode for good: the
|
||||
volumes stay ordinary modifiers without settings.
|
||||
|
||||
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.
|
||||
reader drops them as unknown options and loads a modifier without settings,
|
||||
which 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.
|
||||
|
||||
## Print invalidation
|
||||
### 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.
|
||||
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 these two families, since neither
|
||||
affects object slicing; such a switch also changes the support volumes, so the
|
||||
support step is invalidated as well.
|
||||
|
||||
## Modifier slices
|
||||
A conversion to or from a part or an ordinary modifier changes the solid and
|
||||
modifier volume lists and reslices the object as before. The volume keeps its
|
||||
ID across the type change, so the region cache treats a former support or
|
||||
Precise Seam volume that became a part or modifier as new, since it was never
|
||||
cached.
|
||||
|
||||
`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.
|
||||
Removing the last helper of a single-part object reslices it, as removing any
|
||||
last modifier would.
|
||||
|
||||
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
|
||||
### 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()`
|
||||
modifiers: the menu does not offer it, 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.
|
||||
*Precise Seam Type* submenu appears only when every selected item is a Precise
|
||||
Seam volume, including settings rows that resolve to one, and sets the chosen
|
||||
mode on all of them.
|
||||
- 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 `ModelVolumeType` values plus the `is_precise_seam*()`
|
||||
methods.
|
||||
view, at 60% opacity: warm orange, gold and dark orange for Center, Left and
|
||||
Right; green, red and gray for Enforced, Blocked and Neutral. The three strong
|
||||
colors are close shades of one orange because all three mark strong
|
||||
modifiers; the object list icons tell the modes apart.
|
||||
- Precise Seam volumes have no filament and cannot be pasted into SLA objects.
|
||||
Python plugins see them as `ModelVolumeType` values and through the
|
||||
`is_precise_seam*()` methods.
|
||||
|
||||
## Implementation and verification
|
||||
|
||||
- [PreciseSeam.cpp](../../src/libslic3r/GCode/PreciseSeam.cpp) implements segment
|
||||
detection, point insertion, weak-zone resolution and position restoration.
|
||||
[SeamPlacer.cpp](../../src/libslic3r/GCode/SeamPlacer.cpp) integrates it into
|
||||
candidate gathering and issues the warning.
|
||||
- [PreciseSeam.cpp](../../src/libslic3r/GCode/PreciseSeam.cpp) implements the
|
||||
modifier cache, perimeter preparation, segment extraction and binding, strong
|
||||
selection and insertion, weak-zone preparation and application, and position
|
||||
restoration. [PreciseSeam.hpp](../../src/libslic3r/GCode/PreciseSeam.hpp)
|
||||
declares the contracts; [PreciseSeamInternal.hpp](../../src/libslic3r/GCode/PreciseSeamInternal.hpp)
|
||||
exposes the binding internals to tests.
|
||||
- [SeamPlacer.cpp](../../src/libslic3r/GCode/SeamPlacer.cpp) fills the cache,
|
||||
normalizes perimeters, calls both consumers while gathering candidates,
|
||||
restores strong positions after alignment and prepares the warning text, which
|
||||
[GCode.cpp](../../src/libslic3r/GCode.cpp) issues during G-code export.
|
||||
- [Model.hpp](../../src/libslic3r/Model.hpp) defines the types and their order,
|
||||
[PrintApply.cpp](../../src/libslic3r/PrintApply.cpp) handles invalidation, and
|
||||
[PrintObjectSlice.cpp](../../src/libslic3r/PrintObjectSlice.cpp) slices the
|
||||
modifiers. [bbs_3mf.cpp](../../src/libslic3r/Format/bbs_3mf.cpp) and
|
||||
[3mf.cpp](../../src/libslic3r/Format/3mf.cpp) store them.
|
||||
[PrintObjectSlice.cpp](../../src/libslic3r/PrintObjectSlice.cpp) slices single
|
||||
volumes into structured regions. [bbs_3mf.cpp](../../src/libslic3r/Format/bbs_3mf.cpp)
|
||||
and [3mf.cpp](../../src/libslic3r/Format/3mf.cpp) store them.
|
||||
- [GUI_Factories.cpp](../../src/slic3r/GUI/GUI_Factories.cpp) and
|
||||
[GUI_ObjectList.cpp](../../src/slic3r/GUI/GUI_ObjectList.cpp) provide the menus,
|
||||
type changes and ordering.
|
||||
type changes and ordering; [3DScene.cpp](../../src/slic3r/GUI/3DScene.cpp)
|
||||
defines the colors.
|
||||
- [Segment extraction tests](../../tests/libslic3r/test_precise_seam.cpp) cover
|
||||
clipping and binding: holes and components, contour origin and reversal,
|
||||
repeated coordinates, collinear vertices and rounding, rollback and the
|
||||
diagnostic limits, the rounding fallback on synthetic and real Clipper
|
||||
fragments, contacts, and full containment including touches and sub-micron
|
||||
gaps on inclined edges and around vertices.
|
||||
- [Precise Seam tests](../../tests/fff_print/test_precise_seam.cpp) 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.
|
||||
consumers: strong targets in every mode, including a midpoint on an existing
|
||||
vertex or the closing edge, longest-arc selection and tie order in bed axes,
|
||||
priorities, weak boundaries that coincide or share an edge, enforced
|
||||
subdivision, whole-perimeter weak zones with painting and priorities, weak
|
||||
zones over painting's oversampled candidates, the warning type masks, usage
|
||||
tracking for the "had no effect" warning, volume sorting of strong and weak
|
||||
groups, restoration of strong points after alignment, raft layer indexing and
|
||||
structured slices. End-to-end tests slice a real object with Precise Seam
|
||||
volumes and check the outer wall starts in the exported G-code: every strong
|
||||
mode under several seam positions and with a raft, Enforced and Blocked zones,
|
||||
a modifier with a hole, and the user warning.
|
||||
- [Seam placer tests](../../tests/fff_print/test_seam_placer.cpp) 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.
|
||||
contours, duplicate removal, and `Print::apply()` synchronization through type
|
||||
changes and restored model snapshots. The duplicate-removal test also checks
|
||||
the "had no effect" warning text prepared by `init()` for a helper that never
|
||||
reaches the loop. Further tests check that adding, moving, retyping or
|
||||
removing a Precise Seam volume invalidates only G-code export, and that seam
|
||||
painting acts only from model parts and negative volumes, including after a
|
||||
type change back to part.
|
||||
- [3MF tests](../../tests/libslic3r/test_precise_seam_3mf.cpp) cover the round
|
||||
trip of every mode and of inactive settings, attribute escaping, and which
|
||||
metadata combinations restore a seam mode.
|
||||
trip of every mode and of inactive settings, attribute escaping, and the
|
||||
metadata combinations that restore a seam mode.
|
||||
[Plugin tests](../../tests/slic3rutils/test_precise_seam_plugin.cpp) cover the
|
||||
Python bindings.
|
||||
|
||||
@@ -22855,3 +22855,28 @@ msgstr ""
|
||||
#: resources/data/hints.ini: [hint:Avoid warping]
|
||||
msgid "Avoid warping\nDid you know that when printing materials that are prone to warping such as ABS, appropriately increasing the heatbed temperature can reduce the probability of warping?"
|
||||
msgstr ""
|
||||
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "failed to process some intersections (%1%)"
|
||||
msgstr ""
|
||||
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "multiple intersections with a perimeter, only one was used (%1%)"
|
||||
msgstr ""
|
||||
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)"
|
||||
msgstr ""
|
||||
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "modifier \"%1%\" of \"%2%\" had no effect on the seam (it might not reach the centerline of the printed perimeter)"
|
||||
msgstr ""
|
||||
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "modifier \"%1%\" of \"%2%\" (%3% in total) had no effect on the seam (it might not reach the centerline of the printed perimeter)"
|
||||
msgstr ""
|
||||
|
||||
@@ -27362,17 +27362,30 @@ msgstr ""
|
||||
msgid "Precise Seam"
|
||||
msgstr "Точный шов"
|
||||
|
||||
msgid "multiple intersections with a perimeter detected"
|
||||
msgstr "обнаружены множественные пересечения с периметром"
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "failed to process some intersections (%1%)"
|
||||
msgstr "не удалось обработать некоторые пересечения (%1%)"
|
||||
|
||||
msgid "modifier fully crosses the printable perimeter"
|
||||
msgstr "модификатор пересекает печатаемый периметр насквозь"
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "multiple intersections with a perimeter, only one was used (%1%)"
|
||||
msgstr "несколько пересечений с периметром, использовано только одно (%1%)"
|
||||
|
||||
msgid "modifier shape is not solid (has holes inside) and was ignored"
|
||||
msgstr "форма модификатора не сплошная (имеет отверстия) и была проигнорирована"
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)"
|
||||
msgstr "периметр целиком внутри модификатора, модификатор для него не применён (%1%)"
|
||||
|
||||
msgid "perimeter is fully contained inside modifier and was ignored"
|
||||
msgstr "периметр полностью содержится внутри модификатора и был проигнорирован"
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "modifier \"%1%\" of \"%2%\" had no effect on the seam (it might not reach the centerline of the printed perimeter)"
|
||||
msgstr "модификатор «%1%» модели «%2%» не повлиял на шов (возможно, он не достигает осевой линии печатаемого периметра)"
|
||||
|
||||
#: src/libslic3r/GCode/SeamPlacer.cpp
|
||||
#, possible-boost-format
|
||||
msgid "modifier \"%1%\" of \"%2%\" (%3% in total) had no effect on the seam (it might not reach the centerline of the printed perimeter)"
|
||||
msgstr "модификатор «%1%» модели «%2%» (всего: %3%) не повлиял на шов (возможно, он не достигает осевой линии печатаемого периметра)"
|
||||
|
||||
msgid "Seam placement may differ from expected."
|
||||
msgstr "Размещение шва может отличаться от ожидаемого."
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<!-- Рамка -->
|
||||
<!-- Frame -->
|
||||
<path d="M0.5,13.5L0.5,1.5C0.5,0.951,0.951,0.5,1.5,0.5L13.5,0.5C14.049,0.5,14.5,0.951,14.5,1.5L14.5,13.5C14.5,14.049,14.049,14.5,13.5,14.5L1.5,14.5C0.951,14.5,0.5,14.049,0.5,13.5Z" style="fill:none;stroke:#949494;stroke-width:1px"/>
|
||||
|
||||
<!-- Центральные пунктирные линии -->
|
||||
<!-- Dashed seam line, centered -->
|
||||
<rect x="7" y="1" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="3" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="5" width="1" height="1" style="fill:#009688"/>
|
||||
@@ -11,9 +11,9 @@
|
||||
<rect x="7" y="11" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="13" width="1" height="1" style="fill:#009688"/>
|
||||
|
||||
<!-- Правая стрелка -->
|
||||
<path d="M9.5,7.5L12.504,4.504L12.5,10.5L9.5,7.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
<!-- Left arrow -->
|
||||
<path d="M9.5,7.5L12.5,4.5L12.5,10.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
|
||||
<!-- Левая стрелка -->
|
||||
<path d="M5.501928,7.5L2.497928,4.504L2.501928,10.5L5.501928,7.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
<!-- Right arrow -->
|
||||
<path d="M5.5,7.5L2.5,4.5L2.5,10.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,16 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<!-- Рамка -->
|
||||
<!-- Frame -->
|
||||
<path d="M0.5,13.5L0.5,1.5C0.5,0.951,0.951,0.5,1.5,0.5L13.5,0.5C14.049,0.5,14.5,0.951,14.5,1.5L14.5,13.5C14.5,14.049,14.049,14.5,13.5,14.5L1.5,14.5C0.951,14.5,0.5,14.049,0.5,13.5Z" style="fill:none;stroke:#949494;stroke-width:1px"/>
|
||||
|
||||
<!-- Центральные пунктирные линии -->
|
||||
<rect x="7" y="1" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="3" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="5" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="7" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="9" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="11" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="13" width="1" height="1" style="fill:#009688"/>
|
||||
<!-- Dashed seam line, shifted left -->
|
||||
<rect x="3" y="1" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="3" y="3" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="3" y="5" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="3" y="7" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="3" y="9" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="3" y="11" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="3" y="13" width="1" height="1" style="fill:#009688"/>
|
||||
|
||||
<!-- Правая стрелка -->
|
||||
<path d="M9.5,7.5L12.504,4.504L12.5,10.5L9.5,7.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
<!-- Left arrow -->
|
||||
<path d="M5.5,7.5L8.5,4.5L8.5,10.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1018 B |
@@ -1,16 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<!-- Рамка -->
|
||||
<!-- Frame -->
|
||||
<path d="M0.5,13.5L0.5,1.5C0.5,0.951,0.951,0.5,1.5,0.5L13.5,0.5C14.049,0.5,14.5,0.951,14.5,1.5L14.5,13.5C14.5,14.049,14.049,14.5,13.5,14.5L1.5,14.5C0.951,14.5,0.5,14.049,0.5,13.5Z" style="fill:none;stroke:#949494;stroke-width:1px"/>
|
||||
|
||||
<!-- Центральные пунктирные линии -->
|
||||
<rect x="7" y="1" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="3" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="5" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="7" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="9" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="11" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="7" y="13" width="1" height="1" style="fill:#009688"/>
|
||||
<!-- Dashed seam line, shifted right -->
|
||||
<rect x="11" y="1" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="11" y="3" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="11" y="5" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="11" y="7" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="11" y="9" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="11" y="11" width="1" height="1" style="fill:#009688"/>
|
||||
<rect x="11" y="13" width="1" height="1" style="fill:#009688"/>
|
||||
|
||||
<!-- Левая стрелка -->
|
||||
<path d="M5.501928,7.5L2.497928,4.504L2.501928,10.5L5.501928,7.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
<!-- Right arrow -->
|
||||
<path d="M9.5,7.5L6.5,4.5L6.5,10.5Z" style="fill:#949494;stroke:#949494;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -260,6 +260,7 @@ set(lisbslic3r_sources
|
||||
GCode/SeamPlacer.hpp
|
||||
GCode/PreciseSeam.cpp
|
||||
GCode/PreciseSeam.hpp
|
||||
GCode/PreciseSeamInternal.hpp
|
||||
#GCodeSender.cpp
|
||||
#GCodeSender.hpp
|
||||
GCode/SmallAreaInfillFlowCompensator.cpp
|
||||
|
||||
@@ -3888,6 +3888,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
// Collect custom seam data from all objects.
|
||||
std::function<void(void)> throw_if_canceled_func = [&print]() { print.throw_if_canceled(); };
|
||||
m_seam_placer.init(print, throw_if_canceled_func);
|
||||
// Precise Seam: init() only prepares its warning; issue it here, inside the active export step.
|
||||
if (!m_seam_placer.precise_seam_warning().empty())
|
||||
print.active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL,
|
||||
m_seam_placer.precise_seam_warning(),
|
||||
PrintStateBase::SlicingPreciseSeamWarning);
|
||||
|
||||
// BBS: get path for change filament
|
||||
if (m_writer.multiple_extruders) {
|
||||
|
||||
+667
-977
File diff suppressed because it is too large
Load Diff
@@ -2,47 +2,22 @@
|
||||
#define slic3r_PreciseSeam_hpp_
|
||||
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "SeamPlacer.hpp"
|
||||
|
||||
// CURRENT STATUS:
|
||||
// Strong modifiers (Center/Left/Right): only one intersection per perimeter is supported,
|
||||
// since there can be only one seam. Additional intersections are ignored.
|
||||
//
|
||||
// Weak modifiers (Enforced/Blocked/Neutral): multiple intersections are supported,
|
||||
// but none of them should pass through the model entirely. A through-body intersection
|
||||
// produces multiple segments, of which only one will be processed.
|
||||
//
|
||||
// In both cases, a pop-up warning is shown when unsupported intersections are detected.
|
||||
//
|
||||
// If any modifier has a multiply-connected cross-section (e.g. a hollow shape),
|
||||
// it is skipped and a corresponding notification is shown.
|
||||
//
|
||||
// Full containment of the perimeter within modifier is not handled.
|
||||
//
|
||||
// FUTURE DIRECTION:
|
||||
// A lightweight algorithm is needed to detect and handle through-body intersections
|
||||
// for Weak modifiers. The algorithm must not slow down the 99.9% common case.
|
||||
// Possible approach: if intersection passes the diff check (no through-body),
|
||||
// use the current fast algorithm. If diff check fails, fall back to a heavier
|
||||
// method: compute midpoints of intersection polygon edges, then check which
|
||||
// midpoints lie strictly inside the modifier (not on boundary) using
|
||||
// point_in_polygon. Those edges originate from the perimeter; the rest
|
||||
// originate from the modifier boundary. Collect perimeter edges into a polyline.
|
||||
// Additionally, multiply-connected cross-sections could be supported instead of
|
||||
// being skipped entirely (e.g. by decomposing them into simple polygons).
|
||||
// For Enforced and Neutral weak modifiers, full containment of the perimeter
|
||||
// within modifier could be handled (currently ignored).
|
||||
// Precise Seam: helper volumes that decide where the seam goes on external perimeters.
|
||||
// Design: docs/HLSD/precise-seam.md
|
||||
|
||||
namespace Slic3r {
|
||||
namespace PreciseSeam {
|
||||
@@ -50,88 +25,158 @@ namespace PreciseSeam {
|
||||
// Import EnforcedBlockedSeamPoint from SeamPlacerImpl namespace for convenience
|
||||
using SeamPlacerImpl::EnforcedBlockedSeamPoint;
|
||||
|
||||
// Pre-sliced modifier cache: ModelVolume pointer → per-layer Polygons.
|
||||
// Built once in SeamPlacer::init(), then passed read-only into per-perimeter functions.
|
||||
using ModifierSlicesCache = std::unordered_map<const ModelVolume*, std::vector<Polygons>>;
|
||||
// Geometry and its exterior bounds are prepared together, then treated as read-only.
|
||||
struct ModifierRegion {
|
||||
ExPolygon polygon;
|
||||
BoundingBox bounds;
|
||||
|
||||
// Warning flags set during Precise Seam processing (thread-safe)
|
||||
explicit ModifierRegion(ExPolygon region)
|
||||
: polygon(std::move(region)), bounds(polygon.contour.points) {}
|
||||
};
|
||||
|
||||
using ModifierRegions = std::vector<ModifierRegion>;
|
||||
using ModifierSlices = std::vector<ModifierRegions>;
|
||||
// Per-volume slices with cached bounds, shared read-only by both modifier kinds.
|
||||
using ModifierRegionsCache = std::unordered_map<const ModelVolume*, ModifierSlices>;
|
||||
|
||||
// Move sliced geometry into the cache without detaching holes or changing layer indices.
|
||||
ModifierRegions prepare_modifier_regions(ExPolygons regions);
|
||||
ModifierSlices prepare_modifier_slices(std::vector<ExPolygons> slices);
|
||||
|
||||
// Bound diagnostic volume only; every failed or recovered fragment is still counted and handled.
|
||||
// The same limit applies separately to failure and recovery markers.
|
||||
inline constexpr size_t failed_fragment_log_limit = 10;
|
||||
|
||||
// Shared by all layers and objects in one SeamPlacer::init(); a new pass starts fresh.
|
||||
struct PreciseSeamWarnings {
|
||||
std::atomic<bool> multiple_intersections{false}; // modifier intersects perimeter in multiple separate places (strong only)
|
||||
std::atomic<bool> through_body{false}; // modifier passes through the model body entirely
|
||||
std::atomic<bool> multiply_connected{false}; // modifier has holes (multiply-connected cross-section)
|
||||
std::atomic<bool> full_containment{false}; // modifier fully contains perimeter, no intersection edges
|
||||
// Masks of the Precise Seam types that caused each warning reason, one bit per type (type_bit()).
|
||||
// The user warning lists the types instead of naming modifiers.
|
||||
std::atomic<unsigned> multiple_intersections{0}; // Center/Left/Right with several segments on a perimeter.
|
||||
std::atomic<unsigned> full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right, Blocked.
|
||||
std::atomic<unsigned> failed_types{0}; // Types with at least one discarded fragment.
|
||||
std::atomic<size_t> failed_fragments{0}; // Total discarded fragments, for the log summary.
|
||||
// Fragments saved by the rare-case fallback or accepted as contacts; log only, no user warning.
|
||||
// Clipper is deterministic, so a prismatic model can repeat the same case on every layer.
|
||||
std::atomic<size_t> recovered_fragments{0};
|
||||
|
||||
// Per-modifier flags for the "had no effect" warning. Modifiers are registered before the parallel
|
||||
// phase, so workers only set flags; unregistered ones (e.g. in tests) are not tracked.
|
||||
struct ModifierUsage {
|
||||
std::atomic<bool> checked{false}; // Extracted on at least one perimeter.
|
||||
std::atomic<bool> reached{false}; // Gave a segment, full containment or a discarded fragment.
|
||||
};
|
||||
std::unordered_map<const ModelVolume*, ModifierUsage> modifier_usage;
|
||||
|
||||
// Bit of a Precise Seam type in the masks above, in menu order (Center is bit 0).
|
||||
static unsigned type_bit(ModelVolumeType type)
|
||||
{
|
||||
assert(is_precise_seam(type));
|
||||
return 1u << (int(type) - int(ModelVolumeType::PRECISE_SEAM_CENTER));
|
||||
}
|
||||
// Load before fetch_or: most calls find the bit already set, so shared cache lines stay clean.
|
||||
static void mark(std::atomic<unsigned> &mask, ModelVolumeType type)
|
||||
{
|
||||
const unsigned bit = type_bit(type);
|
||||
if ((mask.load(std::memory_order_relaxed) & bit) == 0)
|
||||
mask.fetch_or(bit, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
// Result of finding common segment between perimeter and intersection
|
||||
struct SegmentData {
|
||||
Polyline segment; // Points from intersection_polygon forming the segment
|
||||
std::vector<size_t> perimeter_edge_indices; // edge_index for each point in segment
|
||||
// Optional caller identity for concise diagnostics when an intersection is discarded.
|
||||
struct ExtractionContext {
|
||||
const Layer *layer = nullptr;
|
||||
const ModelVolume *modifier = nullptr;
|
||||
PreciseSeamWarnings *warnings = nullptr;
|
||||
};
|
||||
|
||||
// Borrows the source polygon; use only until insertion/refinement changes that polygon.
|
||||
struct PreparedPerimeter {
|
||||
const Polygon &polygon;
|
||||
BoundingBox bounds;
|
||||
Polyline line;
|
||||
bool valid = false;
|
||||
|
||||
explicit PreparedPerimeter(const Polygon &perimeter);
|
||||
};
|
||||
|
||||
// A vertex is represented by its outgoing edge and parameter zero, including vertex 0.
|
||||
struct PerimeterPosition {
|
||||
size_t edge_index;
|
||||
double parameter;
|
||||
};
|
||||
|
||||
// Prepared against the immutable perimeter, before insertion shifts its edge indices.
|
||||
struct StrongSeamTarget {
|
||||
Point point;
|
||||
size_t edge_index;
|
||||
};
|
||||
|
||||
struct PerimeterSegment {
|
||||
Polyline polyline;
|
||||
// One bound source edge per polyline interval.
|
||||
std::vector<size_t> edge_indices;
|
||||
PerimeterPosition begin;
|
||||
PerimeterPosition end;
|
||||
std::optional<StrongSeamTarget> strong_target; // Absent for weak modifiers and full containment.
|
||||
// Scaled arc length, calculated only for Center or comparison of multiple strong segments.
|
||||
double length = 0.; // Zero means unmeasured for weak, full containment, and a single Left/Right segment.
|
||||
};
|
||||
|
||||
struct SegmentExtraction {
|
||||
std::vector<PerimeterSegment> segments;
|
||||
bool full_containment = false;
|
||||
bool valid = true; // Invalid perimeter input; discarded fragments do not invalidate other segments.
|
||||
// Clipped fragments whose binding failed; one segment may consist of several fragments.
|
||||
size_t discarded_fragments = 0; // Failed bindings are ignored, with a warning and diagnostic marker.
|
||||
};
|
||||
|
||||
// Clips a prepared, unchanged perimeter (>= 3 vertices, no consecutive duplicates, either direction)
|
||||
// against a modifier's regions and returns its segments; strong targets are prepared only where needed.
|
||||
SegmentExtraction extract_perimeter_segments(const PreparedPerimeter &prepared, const ModifierRegions &modifier,
|
||||
ModelVolumeType mode, const ExtractionContext &context = {});
|
||||
|
||||
// Result of weak modifier segment processing
|
||||
struct WeakModifierSegment {
|
||||
EnforcedBlockedSeamPoint type; // Enforced/Blocked/Neutral
|
||||
Point left_point; // Coordinates of left (first) point of segment
|
||||
size_t left_idx; // Perimeter vertex index for left_point
|
||||
PerimeterPosition left_position; // Position on the source perimeter before insertion/refinement.
|
||||
Point right_point; // Coordinates of right (last) point of segment
|
||||
size_t right_idx; // Perimeter vertex index for right_point
|
||||
PerimeterPosition right_position; // Retained provenance, not an index into the modified polygon.
|
||||
// Full containment of an Enforced or Neutral modifier: the zone is the whole perimeter, without
|
||||
// boundaries (the points and positions above are unused and nothing is inserted for it).
|
||||
bool whole_perimeter = false;
|
||||
};
|
||||
|
||||
// Initialize Precise Seam data by populating provided vectors and flag
|
||||
// Collects precise seam modifiers and fills output parameters
|
||||
// Call once during SeamPlacer::init() before gather_seam_candidates()
|
||||
// Parameters:
|
||||
// strong_volumes_out - output vector for strong modifiers (CENTER/LEFT/RIGHT)
|
||||
// weak_volumes_out - output vector for weak modifiers (ENFORCED/BLOCKED/NEUTRAL)
|
||||
// has_strong_out - output flag indicating presence of strong modifiers
|
||||
// model_object - model object containing volumes
|
||||
// Collects the object's Precise Seam volumes: strong ones in priority order, weak ones in application
|
||||
// order. Call once per object in SeamPlacer::init() before gathering candidates.
|
||||
void init_precise_seam_data(
|
||||
std::vector<const ModelVolume*>& strong_volumes_out,
|
||||
std::vector<const ModelVolume*>& weak_volumes_out,
|
||||
bool& has_strong_out,
|
||||
const ModelObject* model_object);
|
||||
|
||||
// Insert strong seam point into perimeter polygon
|
||||
// Processes strong modifiers (CENTER/LEFT/RIGHT) and inserts seam point into polygon
|
||||
// Parameters:
|
||||
// strong_volumes - list of strong precise seam modifiers
|
||||
// polygon - perimeter polygon (will be modified if point inserted)
|
||||
// layer - current layer
|
||||
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
|
||||
// Returns:
|
||||
// Coordinates of inserted point (internal units) or std::nullopt if nothing inserted
|
||||
// Inserts the seam point of the first strong modifier with a usable segment on this perimeter and returns
|
||||
// it, or nullopt. `prepared` must describe `polygon` before any change.
|
||||
std::optional<Point> insert_strong_seam_point(
|
||||
const std::vector<const ModelVolume*> &strong_volumes,
|
||||
Polygon &polygon,
|
||||
const PreparedPerimeter &prepared,
|
||||
const Layer *layer,
|
||||
const ModifierSlicesCache &slices_cache,
|
||||
const ModifierRegionsCache &slices_cache,
|
||||
PreciseSeamWarnings* warnings = nullptr);
|
||||
|
||||
// Collect all weak modifier segments for a perimeter polygon
|
||||
// Processes weak modifiers (ENFORCED/BLOCKED/NEUTRAL) and collects segment boundaries
|
||||
// Also inserts boundary points into the perimeter polygon (sorted by descending arc length)
|
||||
// Refines enforced edges by subdividing them into segments ≤ enforcer_oversampling_distance
|
||||
// Parameters:
|
||||
// weak_volumes - list of weak precise seam modifiers
|
||||
// polygon - perimeter polygon (will be modified with inserted points and refined edges)
|
||||
// layer - current layer
|
||||
// slices_cache - pre-sliced modifier polygons (built once in SeamPlacer::init)
|
||||
// Returns:
|
||||
// Ordered vector of segments with updated coordinates (same order as weak_volumes list)
|
||||
// Collects weak zones, inserts their boundaries into `polygon` and subdivides enforced edges. Pass
|
||||
// modifiers lowest priority first; `prepared` must describe the unchanged `polygon`.
|
||||
std::vector<WeakModifierSegment> collect_weak_modifier_segments(
|
||||
const std::vector<const ModelVolume*> &weak_volumes,
|
||||
Polygon &polygon,
|
||||
const PreparedPerimeter &prepared,
|
||||
const Layer *layer,
|
||||
const ModifierSlicesCache &slices_cache,
|
||||
const ModifierRegionsCache &slices_cache,
|
||||
PreciseSeamWarnings* warnings = nullptr);
|
||||
|
||||
// Apply weak modifier types to perimeter points based on segment boundaries
|
||||
// Finds boundary points in refined polygon and sets types for points within segments
|
||||
// Parameters:
|
||||
// weak_segments - segments with boundary coordinates and types
|
||||
// result - layer seams data to modify
|
||||
// perimeter - perimeter info (start/end indices)
|
||||
// some_point_enforced - flag to update if Enforced points are set
|
||||
// Retypes the candidates inside each zone in the given order (pass zones lowest priority first);
|
||||
// sets some_point_enforced when an Enforced zone applies.
|
||||
void apply_weak_modifiers_to_perimeter(
|
||||
const std::vector<WeakModifierSegment> &weak_segments,
|
||||
PrintObjectSeamData::LayerSeams &result,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "PreciseSeam.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r::PreciseSeam::detail {
|
||||
|
||||
// Binding intermediates retain source edge identity until segment assembly.
|
||||
struct ClippedEdgeInterval {
|
||||
size_t edge;
|
||||
double begin;
|
||||
double end;
|
||||
Point first;
|
||||
Point last;
|
||||
};
|
||||
|
||||
struct FragmentBindingFailure {
|
||||
size_t pair_index = 0;
|
||||
const char *reason = "empty fragment";
|
||||
};
|
||||
|
||||
// Failure rolls back this fragment only; earlier bindings remain intact.
|
||||
bool append_projected_fragment(const Polyline &fragment, const Polygon &perimeter,
|
||||
std::vector<ClippedEdgeInterval> &intervals,
|
||||
FragmentBindingFailure &failure);
|
||||
|
||||
// Exact path, then projection path, without the fallback; intervals are unchanged on failure.
|
||||
// Exposed so tests can show that a fragment needs the fallback in append_fragment().
|
||||
bool bind_fragment(const Polyline &fragment, const Polygon &perimeter,
|
||||
std::vector<ClippedEdgeInterval> &intervals, FragmentBindingFailure &failure);
|
||||
|
||||
// Binds one fragment. After a failure it tries the rare-case repair and the contact rule, both logged
|
||||
// as recoveries. Returns false when the fragment is discarded.
|
||||
bool append_fragment(const Polyline &fragment, const Polygon &perimeter,
|
||||
std::vector<ClippedEdgeInterval> &intervals,
|
||||
const ExtractionContext &context, size_t fragment_index);
|
||||
|
||||
} // namespace Slic3r::PreciseSeam::detail
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "tbb/parallel_reduce.h"
|
||||
#include <atomic>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/format.hpp>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstddef>
|
||||
@@ -334,10 +335,8 @@ struct GlobalModelInfo {
|
||||
// Precise Seam modifiers: weak modifiers (ENFORCED/BLOCKED/NEUTRAL) provide hints for seam placement
|
||||
std::vector<const ModelVolume*> precise_seam_weak_volumes;
|
||||
|
||||
// Pre-sliced modifier polygons, keyed by ModelVolume pointer.
|
||||
// Populated once in SeamPlacer::init() to avoid re-slicing on every perimeter.
|
||||
// Each value is a per-layer vector of Polygons for that modifier volume.
|
||||
std::unordered_map<const ModelVolume*, std::vector<Polygons>> precise_seam_slices;
|
||||
// Slice each modifier once; both consumers share structured regions and source provenance.
|
||||
PreciseSeam::ModifierRegionsCache precise_seam_slices;
|
||||
|
||||
bool is_enforced(const Vec3f &position, float radius) const {
|
||||
if (enforcers.empty()) {
|
||||
@@ -518,22 +517,26 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
|
||||
const auto& strong_volumes = global_model_info.precise_seam_strong_volumes;
|
||||
const auto& weak_volumes = global_model_info.precise_seam_weak_volumes;
|
||||
|
||||
// Use pre-sliced cache from global_model_info instead of re-slicing on every call
|
||||
auto seam_point = PreciseSeam::insert_strong_seam_point(strong_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
|
||||
std::optional<Point> seam_point;
|
||||
std::vector<PreciseSeam::WeakModifierSegment> weak_segments;
|
||||
if (layer != nullptr && (!strong_volumes.empty() || !weak_volumes.empty())) {
|
||||
// Share validation, bounds and clipping line across all modifiers while the polygon is unchanged.
|
||||
// A strong insertion ends processing; otherwise weak reads the same preparation before inserting.
|
||||
const PreciseSeam::PreparedPerimeter prepared(polygon);
|
||||
seam_point = PreciseSeam::insert_strong_seam_point(
|
||||
strong_volumes, polygon, prepared, layer, global_model_info.precise_seam_slices, warnings);
|
||||
if (!seam_point.has_value())
|
||||
weak_segments = PreciseSeam::collect_weak_modifier_segments(
|
||||
weak_volumes, polygon, prepared, layer, global_model_info.precise_seam_slices, warnings);
|
||||
}
|
||||
|
||||
// Store the inserted point position for marking as central_enforcer later
|
||||
// Store the inserted point position for marking as central_enforcer later.
|
||||
std::optional<Vec3f> inserted_seam_position;
|
||||
if (seam_point.has_value()) {
|
||||
Vec2f unscaled_p = unscale(seam_point.value()).cast<float>();
|
||||
inserted_seam_position = Vec3f(unscaled_p.x(), unscaled_p.y(), z_coord);
|
||||
}
|
||||
|
||||
// Process weak modifiers (ENFORCED/BLOCKED/NEUTRAL) only if no strong modifier was inserted
|
||||
std::vector<PreciseSeam::WeakModifierSegment> weak_segments;
|
||||
if (!inserted_seam_position.has_value()) {
|
||||
weak_segments = PreciseSeam::collect_weak_modifier_segments(weak_volumes, polygon, layer, global_model_info.precise_seam_slices, warnings);
|
||||
}
|
||||
|
||||
float angle_arm_len = region != nullptr ? region->flow(FlowRole::frExternalPerimeter).nozzle_diameter() : 0.5f;
|
||||
|
||||
std::vector<float> lengths { };
|
||||
@@ -627,7 +630,8 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
|
||||
patches_starts_ends.push_back(next_index(i));
|
||||
}
|
||||
}
|
||||
//if patches_starts_ends are empty, it means that the whole perimeter is enforced.. don't do anything in that case
|
||||
// If patches_starts_ends are empty, the whole perimeter is enforced, or no point is enforced any more
|
||||
// (Precise Seam weak zones retyped every painted enforcer); don't do anything in either case.
|
||||
if (!patches_starts_ends.empty()) {
|
||||
//if the first point in the patches is not enforced, it marks a patch end. in that case, put it to the end and start on next
|
||||
// to simplify the processing
|
||||
@@ -795,6 +799,12 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
|
||||
auto obj_transform = po->trafo_centered();
|
||||
|
||||
for (const ModelVolume *mv : po->model_object()->volumes) {
|
||||
// Collect painting only from model parts (what the gizmo edits) and negative volumes (the only way
|
||||
// to paint a hole's wall); painting left on modifiers and helpers after a type change is ignored.
|
||||
// TODO: painting on negative volumes still affects the seam, but the gizmo neither shows nor edits it;
|
||||
// making it editable also needs model_custom_seam_data_changed() to track it.
|
||||
if (!mv->is_model_part() && !mv->is_negative_volume())
|
||||
continue;
|
||||
if (mv->is_seam_painted()) {
|
||||
auto model_transformation = obj_transform * mv->get_matrix();
|
||||
|
||||
@@ -1508,9 +1518,10 @@ void SeamPlacer::align_seam_points(const PrintObject *po, const SeamPlacerImpl::
|
||||
|
||||
}
|
||||
|
||||
void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_func) {
|
||||
void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_canceled_func) {
|
||||
using namespace SeamPlacerImpl;
|
||||
m_seam_per_object.clear();
|
||||
m_precise_seam_warning.clear();
|
||||
|
||||
// Warning flags for Precise Seam processing — shared across all objects
|
||||
PreciseSeam::PreciseSeamWarnings precise_seam_warnings;
|
||||
@@ -1529,13 +1540,17 @@ void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_
|
||||
m_seam_per_object[po].has_precise_seam_strong_volumes,
|
||||
po->model_object());
|
||||
|
||||
// Pre-slice all precise seam modifier volumes once per object.
|
||||
// Without this cache, slice_single_volume() would be called for every
|
||||
// modifier × every perimeter × every layer — thousands of redundant slicing operations.
|
||||
// Slice each Precise Seam modifier once per object; both consumers read the cache.
|
||||
for (const ModelVolume* vol : global_model_info.precise_seam_strong_volumes)
|
||||
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
|
||||
global_model_info.precise_seam_slices[vol] = PreciseSeam::prepare_modifier_slices(po->slice_single_volume_regions(vol));
|
||||
for (const ModelVolume* vol : global_model_info.precise_seam_weak_volumes)
|
||||
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
|
||||
global_model_info.precise_seam_slices[vol] = PreciseSeam::prepare_modifier_slices(po->slice_single_volume_regions(vol));
|
||||
// Register usage tracking before the parallel phase; workers only set its flags. Several print
|
||||
// objects of one model object share volumes, and try_emplace keeps what earlier ones recorded.
|
||||
for (const ModelVolume* vol : global_model_info.precise_seam_strong_volumes)
|
||||
precise_seam_warnings.modifier_usage.try_emplace(vol);
|
||||
for (const ModelVolume* vol : global_model_info.precise_seam_weak_volumes)
|
||||
precise_seam_warnings.modifier_usage.try_emplace(vol);
|
||||
|
||||
throw_if_canceled_func();
|
||||
if (configured_seam_preference == spAligned || configured_seam_preference == spNearest || configured_seam_preference == spAlignedBack) {
|
||||
@@ -1603,24 +1618,74 @@ void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_
|
||||
#endif
|
||||
}
|
||||
|
||||
// Show Precise Seam warnings (once for all objects).
|
||||
// Only ONE active_step_add_warning() call — multiple calls generate multiple UI events,
|
||||
// each re-pushing ALL current warnings via Plater handler, causing NotificationManager::append()
|
||||
// to duplicate text within each popup.
|
||||
// Prepare one combined Precise Seam warning; G-code export issues it. Keep it single: separate
|
||||
// warnings would each re-push all warnings and duplicate text in the notification.
|
||||
{
|
||||
const bool mi = precise_seam_warnings.multiple_intersections.load(std::memory_order_relaxed);
|
||||
const bool tb = precise_seam_warnings.through_body.load(std::memory_order_relaxed);
|
||||
const bool mc = precise_seam_warnings.multiply_connected.load(std::memory_order_relaxed);
|
||||
const bool fc = precise_seam_warnings.full_containment.load(std::memory_order_relaxed);
|
||||
const unsigned failed_types = precise_seam_warnings.failed_types.load(std::memory_order_relaxed);
|
||||
const unsigned mi = precise_seam_warnings.multiple_intersections.load(std::memory_order_relaxed);
|
||||
const unsigned fc = precise_seam_warnings.full_containment.load(std::memory_order_relaxed);
|
||||
const size_t failed = precise_seam_warnings.failed_fragments.load(std::memory_order_relaxed);
|
||||
// All workers have finished; cancellation before this point may omit the summary.
|
||||
if (failed > PreciseSeam::failed_fragment_log_limit)
|
||||
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamIntersectionFailed] " << failed
|
||||
<< " fragments discarded; first " << PreciseSeam::failed_fragment_log_limit
|
||||
<< " logged (parallel processing order), " << (failed - PreciseSeam::failed_fragment_log_limit)
|
||||
<< " omitted";
|
||||
// Recoveries are log-only: no user warning, but the same bounded detail and a total.
|
||||
const size_t recovered = precise_seam_warnings.recovered_fragments.load(std::memory_order_relaxed);
|
||||
if (recovered > PreciseSeam::failed_fragment_log_limit)
|
||||
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamFragmentRecovered] " << recovered
|
||||
<< " fragments recovered; first " << PreciseSeam::failed_fragment_log_limit
|
||||
<< " logged (parallel processing order), " << (recovered - PreciseSeam::failed_fragment_log_limit)
|
||||
<< " omitted";
|
||||
// Reasons name the modifier types, as the menu does, not individual modifiers: "Seam Left, Seam
|
||||
// Enforced" in menu order, each type once. The same msgids as the menu share its translations.
|
||||
const auto type_list = [](unsigned mask) {
|
||||
const std::pair<ModelVolumeType, std::string> types[] = {
|
||||
{ModelVolumeType::PRECISE_SEAM_CENTER, _u8L("Seam Center")},
|
||||
{ModelVolumeType::PRECISE_SEAM_LEFT, _u8L("Seam Left")},
|
||||
{ModelVolumeType::PRECISE_SEAM_RIGHT, _u8L("Seam Right")},
|
||||
{ModelVolumeType::PRECISE_SEAM_ENFORCED, _u8L("Seam Enforced")},
|
||||
{ModelVolumeType::PRECISE_SEAM_BLOCKED, _u8L("Seam Blocked")},
|
||||
{ModelVolumeType::PRECISE_SEAM_NEUTRAL, _u8L("Seam Neutral")}};
|
||||
std::string list;
|
||||
for (const auto &[type, name] : types)
|
||||
if (mask & PreciseSeam::PreciseSeamWarnings::type_bit(type))
|
||||
list += (list.empty() ? "" : ", ") + name;
|
||||
return list;
|
||||
};
|
||||
std::vector<std::string> parts;
|
||||
if (mi)
|
||||
parts.push_back(_u8L("multiple intersections with a perimeter detected"));
|
||||
if (tb)
|
||||
parts.push_back(_u8L("modifier fully crosses the printable perimeter"));
|
||||
if (mc)
|
||||
parts.push_back(_u8L("modifier shape is not solid (has holes inside) and was ignored"));
|
||||
if (fc)
|
||||
parts.push_back(_u8L("perimeter is fully contained inside modifier and was ignored"));
|
||||
if (failed_types != 0)
|
||||
parts.push_back((boost::format(_u8L("failed to process some intersections (%1%)")) % type_list(failed_types)).str());
|
||||
if (mi != 0)
|
||||
parts.push_back((boost::format(_u8L("multiple intersections with a perimeter, only one was used (%1%)")) % type_list(mi)).str());
|
||||
if (fc != 0)
|
||||
parts.push_back((boost::format(_u8L("a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)")) % type_list(fc)).str());
|
||||
// Modifiers evaluated somewhere that never reached a perimeter; never-evaluated ones are not reported.
|
||||
// Print and volume order make the named one deterministic; the log lists them all.
|
||||
std::vector<const ModelVolume*> no_effect;
|
||||
for (const PrintObject *po : print.objects())
|
||||
for (const ModelVolume *volume : po->model_object()->volumes) {
|
||||
const auto it = precise_seam_warnings.modifier_usage.find(volume);
|
||||
if (it != precise_seam_warnings.modifier_usage.end() &&
|
||||
it->second.checked.load(std::memory_order_relaxed) &&
|
||||
!it->second.reached.load(std::memory_order_relaxed) &&
|
||||
std::find(no_effect.begin(), no_effect.end(), volume) == no_effect.end())
|
||||
no_effect.push_back(volume);
|
||||
}
|
||||
// The user warning names only the first one; the log lists them all.
|
||||
for (const ModelVolume *volume : no_effect)
|
||||
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamNoEffect] object=\"" << volume->get_object()->name
|
||||
<< "\" modifier=\"" << volume->name << "\"";
|
||||
if (!no_effect.empty()) {
|
||||
const ModelVolume *first = no_effect.front();
|
||||
if (no_effect.size() == 1)
|
||||
parts.push_back((boost::format(_u8L("modifier \"%1%\" of \"%2%\" had no effect on the seam (it might not reach the centerline of the printed perimeter)"))
|
||||
% first->name % first->get_object()->name).str());
|
||||
else
|
||||
parts.push_back((boost::format(_u8L("modifier \"%1%\" of \"%2%\" (%3% in total) had no effect on the seam (it might not reach the centerline of the printed perimeter)"))
|
||||
% first->name % first->get_object()->name % no_effect.size()).str());
|
||||
}
|
||||
if (!parts.empty()) {
|
||||
// One line: the export warnings dialog shows only the first line of each warning.
|
||||
std::string warning_text = _u8L("Precise Seam") + ": ";
|
||||
@@ -1630,10 +1695,7 @@ void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_
|
||||
}
|
||||
warning_text += ". ";
|
||||
warning_text += _u8L("Seam placement may differ from expected.");
|
||||
print.active_step_add_warning(
|
||||
PrintStateBase::WarningLevel::NON_CRITICAL,
|
||||
warning_text,
|
||||
PrintStateBase::SlicingPreciseSeamWarning);
|
||||
m_precise_seam_warning = std::move(warning_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
@@ -154,10 +155,16 @@ public:
|
||||
//The following data structures hold all perimeter points for all PrintObject.
|
||||
std::unordered_map<const PrintObject*, PrintObjectSeamData> m_seam_per_object;
|
||||
|
||||
void init(Print &print, std::function<void(void)> throw_if_canceled_func);
|
||||
void init(const Print &print, std::function<void(void)> throw_if_canceled_func);
|
||||
|
||||
// Precise Seam user warning prepared by the last init(), empty if there is none. init() does not
|
||||
// change the Print: the caller issues the warning where a print step is active (G-code export).
|
||||
const std::string &precise_seam_warning() const { return m_precise_seam_warning; }
|
||||
|
||||
void place_seam(const Layer *layer, ExtrusionLoop &loop, const Point &last_pos, float& overhang) const;
|
||||
private:
|
||||
std::string m_precise_seam_warning;
|
||||
|
||||
void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
|
||||
PreciseSeam::PreciseSeamWarnings* warnings = nullptr);
|
||||
void calculate_candidates_visibility(const PrintObject *po,
|
||||
|
||||
@@ -1272,7 +1272,6 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
|
||||
this->volumes.emplace_back(new ModelVolume(*model_volume));
|
||||
this->volumes.back()->set_model_object(this);
|
||||
}
|
||||
|
||||
this->clear_instances();
|
||||
this->instances.reserve(rhs.instances.size());
|
||||
for (const ModelInstance *model_instance : rhs.instances) {
|
||||
@@ -1311,7 +1310,6 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
|
||||
rhs.volumes.clear();
|
||||
for (ModelVolume *model_volume : this->volumes)
|
||||
model_volume->set_model_object(this);
|
||||
|
||||
this->clear_instances();
|
||||
this->instances = std::move(rhs.instances);
|
||||
rhs.instances.clear();
|
||||
@@ -1435,9 +1433,7 @@ ModelVolume* ModelObject::add_volume_with_shared_mesh(const ModelVolume &other,
|
||||
void ModelObject::delete_volume(size_t idx)
|
||||
{
|
||||
ModelVolumePtrs::iterator i = this->volumes.begin() + idx;
|
||||
ModelVolume* volume_to_delete = *i;
|
||||
|
||||
delete volume_to_delete;
|
||||
delete *i;
|
||||
this->volumes.erase(i);
|
||||
|
||||
if (this->volumes.size() == 1)
|
||||
@@ -1530,6 +1526,7 @@ void ModelObject::sort_volumes(bool full_sort)
|
||||
return vl_type < vr_type;
|
||||
});
|
||||
}
|
||||
|
||||
ModelInstance* ModelObject::add_instance()
|
||||
{
|
||||
ModelInstance* i = new ModelInstance(this);
|
||||
@@ -3889,7 +3886,6 @@ bool model_volume_list_changed(const ModelObject &model_object_old, const ModelO
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
template< typename TypeFilterFn, typename CompareFn>
|
||||
bool model_property_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, TypeFilterFn type_filter, CompareFn compare)
|
||||
{
|
||||
|
||||
@@ -370,7 +370,6 @@ enum class ModelVolumeType : int {
|
||||
};
|
||||
|
||||
// Free functions for checking ModelVolumeType without a ModelVolume object.
|
||||
// Keep in sync with ModelVolume::is_precise_seam*() methods below.
|
||||
inline bool is_precise_seam(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
inline bool is_precise_seam_strong(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_CENTER && t <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
|
||||
inline bool is_precise_seam_weak(ModelVolumeType t) { return t >= ModelVolumeType::PRECISE_SEAM_ENFORCED && t <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
@@ -1007,13 +1006,13 @@ public:
|
||||
bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; }
|
||||
bool is_support_modifier() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_ENFORCER; }
|
||||
// Check if this volume is any of the precise seam modifier subtypes
|
||||
bool is_precise_seam() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
bool is_precise_seam() const { return Slic3r::is_precise_seam(m_type); }
|
||||
// Helper to check if volume is a "strong" Precise Seam type (center, left, right)
|
||||
// Strong modifiers have priority and always appear above weak modifiers in UI
|
||||
bool is_precise_seam_strong() const { return m_type >= ModelVolumeType::PRECISE_SEAM_CENTER && m_type <= ModelVolumeType::PRECISE_SEAM_RIGHT; }
|
||||
bool is_precise_seam_strong() const { return Slic3r::is_precise_seam_strong(m_type); }
|
||||
// Helper to check if volume is a "weak" Precise Seam type (enforced, blocked, neutral)
|
||||
// Weak modifiers always appear below strong modifiers in UI
|
||||
bool is_precise_seam_weak() const { return m_type >= ModelVolumeType::PRECISE_SEAM_ENFORCED && m_type <= ModelVolumeType::PRECISE_SEAM_NEUTRAL; }
|
||||
bool is_precise_seam_weak() const { return Slic3r::is_precise_seam_weak(m_type); }
|
||||
bool is_text() const { return text_configuration.has_value(); }
|
||||
bool is_svg() const { return emboss_shape.has_value() && !text_configuration.has_value(); }
|
||||
bool is_the_only_one_part() const; // behave like an object
|
||||
|
||||
@@ -331,13 +331,10 @@ Points Polygon::concave_points(double angle_threshold) const
|
||||
}
|
||||
|
||||
// Projection of a point onto the polygon.
|
||||
Point Polygon::point_projection(const Point &point, size_t *edge_index) const
|
||||
Point Polygon::point_projection(const Point &point) const
|
||||
{
|
||||
Point proj = point;
|
||||
double dmin = std::numeric_limits<double>::max();
|
||||
// Preserve the existing projection and tie order while optionally tracking its edge.
|
||||
if (edge_index)
|
||||
*edge_index = std::numeric_limits<size_t>::max();
|
||||
if (! this->points.empty()) {
|
||||
for (size_t i = 0; i < this->points.size(); ++ i) {
|
||||
const Point &pt0 = this->points[i];
|
||||
@@ -346,15 +343,11 @@ Point Polygon::point_projection(const Point &point, size_t *edge_index) const
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
proj = pt0;
|
||||
if (edge_index)
|
||||
*edge_index = i;
|
||||
}
|
||||
d = (point - pt1).cast<double>().norm();
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
proj = pt1;
|
||||
if (edge_index)
|
||||
*edge_index = (i + 1) % this->points.size();
|
||||
}
|
||||
Vec2d v1(coordf_t(pt1(0) - pt0(0)), coordf_t(pt1(1) - pt0(1)));
|
||||
coordf_t div = v1.squaredNorm();
|
||||
@@ -367,8 +360,6 @@ Point Polygon::point_projection(const Point &point, size_t *edge_index) const
|
||||
if (d < dmin) {
|
||||
dmin = d;
|
||||
proj = foot;
|
||||
if (edge_index)
|
||||
*edge_index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,7 @@ public:
|
||||
Points convex_points(double angle_threshold = 0.) const;
|
||||
Points concave_points(double angle_threshold = 0.) const;
|
||||
// Projection of a point onto the polygon.
|
||||
// Optional index: start of the closest edge, or the vertex itself for an endpoint.
|
||||
// Empty polygons return the query point and std::numeric_limits<size_t>::max() as the index.
|
||||
Point point_projection(const Point &point, size_t *edge_index = nullptr) const;
|
||||
Point point_projection(const Point &point) const;
|
||||
std::vector<float> parameter_by_length() const;
|
||||
|
||||
//BBS
|
||||
|
||||
@@ -492,10 +492,8 @@ public:
|
||||
std::vector<Polygons> slice_support_volumes(const ModelVolumeType model_volume_type) const;
|
||||
std::vector<Polygons> slice_support_blockers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_BLOCKER); }
|
||||
std::vector<Polygons> slice_support_enforcers() const { return this->slice_support_volumes(ModelVolumeType::SUPPORT_ENFORCER); }
|
||||
// Shared slicing path; multiple volumes are united per layer.
|
||||
std::vector<Polygons> slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const;
|
||||
// Keep Precise Seam volumes separate so their individual priority is preserved.
|
||||
std::vector<Polygons> slice_single_volume(const ModelVolume* volume) const { return this->slice_modifier_volumes({volume}); }
|
||||
// Preserve each connected region and its holes for perimeter clipping.
|
||||
std::vector<ExPolygons> slice_single_volume_regions(const ModelVolume* volume) const;
|
||||
|
||||
// Helpers to project custom facets on slices
|
||||
void project_and_append_custom_facets(bool seam, EnforcerBlockerType type, std::vector<Polygons>& expolys, std::vector<std::pair<Vec3f,Vec3f>>* vertical_points=nullptr) const;
|
||||
|
||||
@@ -740,7 +740,10 @@ void print_objects_regions_invalidate_keep_some_volumes(PrintObjectRegions &prin
|
||||
for (; i_old < old_volumes.size(); ++ i_old)
|
||||
if (old_volumes[i_old]->id() >= new_volumes[i_new]->id())
|
||||
break;
|
||||
if (i_old != old_volumes.size() && old_volumes[i_old]->id() == new_volumes[i_new]->id()) {
|
||||
// IDs survive type changes: an old volume that was not a solid or modifier was never cached,
|
||||
// so treat it as new instead of looking it up.
|
||||
if (i_old != old_volumes.size() && old_volumes[i_old]->id() == new_volumes[i_new]->id() &&
|
||||
model_volume_solid_or_modifier(*old_volumes[i_old])) {
|
||||
if (old_volumes[i_old]->get_matrix().isApprox(new_volumes[i_new]->get_matrix())) {
|
||||
// Reuse the volume.
|
||||
for (; print_object_regions.cached_volume_ids[i_cached_volume] < old_volumes[i_old]->id(); ++ i_cached_volume)
|
||||
|
||||
@@ -1577,19 +1577,12 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_
|
||||
|
||||
std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const
|
||||
{
|
||||
// Supports merge every matching volume; Precise Seam calls the shared slicer one volume at a time.
|
||||
std::vector<const ModelVolume*> volumes;
|
||||
for (const ModelVolume *volume : this->model_object()->volumes)
|
||||
if (volume->type() == model_volume_type)
|
||||
volumes.push_back(volume);
|
||||
return this->slice_modifier_volumes(volumes);
|
||||
}
|
||||
|
||||
std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<const ModelVolume*> &volumes) const
|
||||
{
|
||||
auto it_volume = this->model_object()->volumes.begin();
|
||||
auto it_volume_end = this->model_object()->volumes.end();
|
||||
for (; it_volume != it_volume_end && (*it_volume)->type() != model_volume_type; ++ it_volume) ;
|
||||
std::vector<Polygons> slices;
|
||||
if (!volumes.empty()) {
|
||||
// Share layer heights, transforms and cancellation handling across the selected volumes.
|
||||
if (it_volume != it_volume_end) {
|
||||
// Found at least a single support volume of model_volume_type.
|
||||
std::vector<float> zs = zs_from_layers(this->layers());
|
||||
std::vector<char> merge_layers;
|
||||
bool merge = false;
|
||||
@@ -1597,26 +1590,27 @@ std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<cons
|
||||
auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); });
|
||||
MeshSlicingParamsEx params;
|
||||
params.trafo = this->trafo_centered();
|
||||
for (const ModelVolume *volume : volumes) {
|
||||
std::vector<ExPolygons> slices2 = slice_volume(*volume, zs, params, throw_on_cancel_callback);
|
||||
if (slices.empty()) {
|
||||
slices.reserve(slices2.size());
|
||||
for (ExPolygons &src : slices2)
|
||||
slices.emplace_back(to_polygons(std::move(src)));
|
||||
} else if (!slices2.empty()) {
|
||||
if (merge_layers.empty())
|
||||
merge_layers.assign(zs.size(), false);
|
||||
for (size_t i = 0; i < zs.size(); ++ i) {
|
||||
if (slices[i].empty())
|
||||
slices[i] = to_polygons(std::move(slices2[i]));
|
||||
else if (! slices2[i].empty()) {
|
||||
append(slices[i], to_polygons(std::move(slices2[i])));
|
||||
merge_layers[i] = true;
|
||||
merge = true;
|
||||
for (; it_volume != it_volume_end; ++ it_volume)
|
||||
if ((*it_volume)->type() == model_volume_type) {
|
||||
std::vector<ExPolygons> slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback);
|
||||
if (slices.empty()) {
|
||||
slices.reserve(slices2.size());
|
||||
for (ExPolygons &src : slices2)
|
||||
slices.emplace_back(to_polygons(std::move(src)));
|
||||
} else if (!slices2.empty()) {
|
||||
if (merge_layers.empty())
|
||||
merge_layers.assign(zs.size(), false);
|
||||
for (size_t i = 0; i < zs.size(); ++ i) {
|
||||
if (slices[i].empty())
|
||||
slices[i] = to_polygons(std::move(slices2[i]));
|
||||
else if (! slices2[i].empty()) {
|
||||
append(slices[i], to_polygons(std::move(slices2[i])));
|
||||
merge_layers[i] = true;
|
||||
merge = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (merge) {
|
||||
std::vector<Polygons*> to_merge;
|
||||
to_merge.reserve(zs.size());
|
||||
@@ -1634,4 +1628,16 @@ std::vector<Polygons> PrintObject::slice_modifier_volumes(const std::vector<cons
|
||||
return slices;
|
||||
}
|
||||
|
||||
std::vector<ExPolygons> PrintObject::slice_single_volume_regions(const ModelVolume* volume) const
|
||||
{
|
||||
if (volume == nullptr)
|
||||
return {};
|
||||
// Match the existing slicing heights and centered transform without flattening holes.
|
||||
const std::vector<float> zs = zs_from_layers(this->layers());
|
||||
MeshSlicingParamsEx params;
|
||||
params.trafo = this->trafo_centered();
|
||||
const Print *print = this->print();
|
||||
return slice_volume(*volume, zs, params, [print]() { print->throw_if_canceled(); });
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -193,7 +193,9 @@ ColorRGBA GLVolume::SUPPORT_BLOCKER_COL = {1.0f, 0.3f, 0.3f, 0.4f};
|
||||
|
||||
ColorRGBA GLVolume::MODEL_HIDDEN_COL = {0.f, 0.f, 0.f, 0.3f};
|
||||
|
||||
// Precise Seam modifier colors
|
||||
// Precise Seam modifier colors. Center, Left and Right are deliberately close shades of one orange:
|
||||
// all three are strong modifiers, and distinct hues per mode would turn the scene into a rainbow.
|
||||
// The object list icons tell the modes apart.
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_CENTER_COL = {1.0f, 0.627f, 0.082f, 0.6f}; // FFA015 - orange
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_LEFT_COL = {1.0f, 0.753f, 0.0f, 0.6f}; // FFC000 - golden
|
||||
ColorRGBA GLVolume::PRECISE_SEAM_RIGHT_COL = {1.0f, 0.514f, 0.0f, 0.6f}; // FF8300 - dark orange
|
||||
|
||||
@@ -5804,6 +5804,7 @@ void ObjectList::change_part_type()
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
ModelVolumeType ObjectList::get_selected_volume_type()
|
||||
{
|
||||
ModelVolume* volume = get_selected_model_volume();
|
||||
|
||||
@@ -98,7 +98,6 @@ struct MeshErrorsInfo
|
||||
class ObjectList : public wxDataViewCtrl
|
||||
{
|
||||
public:
|
||||
|
||||
enum SELECTION_MODE
|
||||
{
|
||||
smUndef = 0,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -209,6 +209,61 @@ TEST_CASE("Entirely painted contours keep valid enforced seam candidates", "[Sea
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Seam painting acts only from model parts and negative volumes", "[SeamPlacer]")
|
||||
{
|
||||
// Painting survives a type change, but only model parts expose it in the seam gizmo. A helper
|
||||
// painted while it was a part must not affect the seam once it is a modifier or support volume.
|
||||
// Negative volumes keep it on purpose: it is the only way to paint the wall of a hole they cut.
|
||||
const auto helper_type = GENERATE(ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME,
|
||||
ModelVolumeType::PARAMETER_MODIFIER, ModelVolumeType::SUPPORT_BLOCKER,
|
||||
ModelVolumeType::SUPPORT_ENFORCER);
|
||||
// Precise Seam helpers are left out: on the loop they would retype the candidates themselves.
|
||||
const std::string type_name = ModelVolume::type_to_string(helper_type);
|
||||
CAPTURE(type_name);
|
||||
PipelineFixture fixture;
|
||||
// A 2 mm strip along the front side: its front face lies on the loop's front edge, while its back
|
||||
// face stays farther than the paint radius. As a negative volume it cuts only the strip.
|
||||
ModelVolume *helper = fixture.model.objects.front()->add_volume(TriangleMesh(its_make_cube(20, 2, 0.4)));
|
||||
const ObjectID helper_id = helper->id();
|
||||
{
|
||||
// Paint every face of the helper while it is still a part, then change its type.
|
||||
TriangleSelector selector(helper->mesh());
|
||||
for (size_t i = 0; i < helper->mesh().its.indices.size(); ++i)
|
||||
selector.set_facet(int(i), EnforcerBlockerType::ENFORCER);
|
||||
helper->seam_facets.set(selector);
|
||||
}
|
||||
REQUIRE(helper->is_seam_painted());
|
||||
const auto count_enforced = [&]() {
|
||||
fixture.print.apply(fixture.model, fixture.config);
|
||||
PrintObject &object = fixture.prepare();
|
||||
auto ®ion = clear_first_layer(object);
|
||||
append_loop(region, fixture.points_in_layer(object, {{0, 0}, {20, 0}, {20, 20}, {0, 20}}));
|
||||
SeamPlacer placer;
|
||||
placer.init(fixture.print, [] {});
|
||||
const auto &data = placer.m_seam_per_object.at(&object).layers.front();
|
||||
REQUIRE(data.perimeters.size() == 1);
|
||||
// The helper volume in the print's model copy keeps its painting whatever its type.
|
||||
const auto &volumes = object.model_object()->volumes;
|
||||
const auto it = std::find_if(volumes.begin(), volumes.end(), [&](const ModelVolume *v) { return v->id() == helper_id; });
|
||||
REQUIRE(it != volumes.end());
|
||||
CHECK((*it)->is_seam_painted());
|
||||
return size_t(std::count_if(data.points.begin(), data.points.end(), [](const auto &candidate) {
|
||||
return candidate.type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced;
|
||||
}));
|
||||
};
|
||||
helper->set_type(helper_type);
|
||||
const bool acts = helper_type == ModelVolumeType::MODEL_PART || helper_type == ModelVolumeType::NEGATIVE_VOLUME;
|
||||
if (acts)
|
||||
CHECK(count_enforced() > 0);
|
||||
else
|
||||
CHECK(count_enforced() == 0);
|
||||
if (!acts) {
|
||||
// The painting was ignored, not lost: as a part again the helper enforces candidates.
|
||||
helper->set_type(ModelVolumeType::MODEL_PART);
|
||||
CHECK(count_enforced() > 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Precise Seam removes path junction duplicates but preserves separate visits", "[SeamPlacer][PreciseSeam]")
|
||||
{
|
||||
const bool enable_ps = GENERATE(false, true);
|
||||
@@ -227,7 +282,14 @@ TEST_CASE("Precise Seam removes path junction duplicates but preserves separate
|
||||
const Points outline = fixture.points_in_layer(object, vertices);
|
||||
append_loop(region, outline, true);
|
||||
SeamPlacer placer;
|
||||
// A direct call outside G-code export: init() must not need an active print step.
|
||||
placer.init(fixture.print, [] {});
|
||||
// The helper never reaches the loop, so it is reported, named with its object; no helper, no warning.
|
||||
if (enable_ps) {
|
||||
CHECK(placer.precise_seam_warning().find("had no effect on the seam") != std::string::npos);
|
||||
CHECK(placer.precise_seam_warning().find("\"object.stl\"") != std::string::npos);
|
||||
} else
|
||||
CHECK(placer.precise_seam_warning().empty());
|
||||
const auto &data = placer.m_seam_per_object.at(&object).layers.front();
|
||||
REQUIRE(data.perimeters.size() == 1);
|
||||
// Each separate path contributes both endpoints in ordinary mode; PS removes only adjacent copies.
|
||||
@@ -294,3 +356,48 @@ TEST_CASE("Print apply synchronizes support and seam helpers through type change
|
||||
fixture.print.apply(fixture.model, fixture.config);
|
||||
check_applied(fixture.model);
|
||||
}
|
||||
|
||||
TEST_CASE("Precise Seam volume changes invalidate only G-code export", "[SeamPlacer][PreciseSeam][Print]")
|
||||
{
|
||||
const int change = GENERATE(0, 1, 2, 3); // Add, move, retype, remove.
|
||||
CAPTURE(change);
|
||||
PipelineFixture fixture;
|
||||
ModelObject *model_object = fixture.model.objects.front();
|
||||
// A second helper stays in the object throughout: deleting down to a single volume makes
|
||||
// ModelObject::delete_volume() fold the volume transform into the instances and renew the volume
|
||||
// ID, which legitimately reslices the object regardless of Precise Seam.
|
||||
ModelVolume *keeper = model_object->add_volume(make_cube(1, 1, 1));
|
||||
keeper->set_type(ModelVolumeType::PRECISE_SEAM_NEUTRAL);
|
||||
if (change != 0) {
|
||||
ModelVolume *seam = model_object->add_volume(make_cube(1, 1, 1));
|
||||
seam->set_type(ModelVolumeType::PRECISE_SEAM_CENTER);
|
||||
}
|
||||
fixture.print.apply(fixture.model, fixture.config);
|
||||
// A full export marks every step done, so an invalidated step is visible afterwards.
|
||||
Test::gcode(fixture.print);
|
||||
REQUIRE(fixture.print.objects().size() == 1);
|
||||
const PrintObject *object = fixture.print.objects().front();
|
||||
REQUIRE(fixture.print.is_step_done(psGCodeExport));
|
||||
REQUIRE(object->is_step_done(posSlice));
|
||||
REQUIRE(object->is_step_done(posPerimeters));
|
||||
|
||||
if (change == 0) {
|
||||
ModelVolume *seam = model_object->add_volume(make_cube(1, 1, 1));
|
||||
seam->set_type(ModelVolumeType::PRECISE_SEAM_CENTER);
|
||||
} else {
|
||||
ModelVolume *seam = model_object->volumes.back();
|
||||
REQUIRE(seam->is_precise_seam());
|
||||
if (change == 1) seam->set_offset(Vec3d(2, 3, 0));
|
||||
if (change == 2) seam->set_type(ModelVolumeType::PRECISE_SEAM_ENFORCED);
|
||||
if (change == 3) model_object->delete_volume(model_object->volumes.size() - 1);
|
||||
}
|
||||
fixture.print.apply(fixture.model, fixture.config);
|
||||
|
||||
// The helper takes no part in slicing: the object and its layers are kept, only export reruns.
|
||||
// REQUIRE, not CHECK: a recreated PrintObject means the old one was freed and must not be read.
|
||||
REQUIRE(fixture.print.objects().size() == 1);
|
||||
REQUIRE(fixture.print.objects().front() == object);
|
||||
CHECK_FALSE(fixture.print.is_step_done(psGCodeExport));
|
||||
CHECK(object->is_step_done(posSlice));
|
||||
CHECK(object->is_step_done(posPerimeters));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_3mf.cpp
|
||||
# Round-trip seam metadata and active/dormant volume settings in both formats.
|
||||
test_precise_seam_3mf.cpp
|
||||
# Pure perimeter extraction is independent of Print/Layer fixtures.
|
||||
test_precise_seam.cpp
|
||||
test_aabbindirect.cpp
|
||||
test_appconfig.cpp
|
||||
test_arachne_walls.cpp
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user