Add Precise Seam placement feature (#12974)

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

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<!-- Рамка -->
<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="2" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Правые пунктирные линии -->
<rect x="12" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Круг (blocked seam marker) -->
<circle cx="7.5" cy="7.5" r="3.5" style="fill:#B16F6D"/>
<!-- Горизонтальная линия (минус) -->
<rect x="5" y="7" width="5" height="1" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<!-- Рамка -->
<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"/>
<!-- Правая стрелка -->
<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"/>
<!-- Левая стрелка -->
<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"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<!-- Рамка -->
<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="2" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Правые пунктирные линии -->
<rect x="12" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Круг (enforced seam marker) -->
<circle cx="7.5" cy="7.5" r="3.5" style="fill:#009688"/>
<!-- Крест внутри круга -->
<rect x="7" y="5" width="1" height="5" style="fill:#fff"/>
<rect x="5" y="7" width="5" height="1" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<!-- Рамка -->
<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"/>
<!-- Правая стрелка -->
<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"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<!-- Рамка -->
<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="2" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Правые пунктирные линии -->
<rect x="12" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Круг (neutral seam marker) -->
<circle cx="7.5" cy="7.5" r="3.5" style="fill:#949494"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<!-- Рамка -->
<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"/>
<!-- Левая стрелка -->
<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"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<!-- Рамка -->
<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="2" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="2" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Правые пунктирные линии -->
<rect x="12" y="1" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="3" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="5" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="7" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="9" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="11" width="1" height="1" style="fill:#009688"/>
<rect x="12" y="13" width="1" height="1" style="fill:#009688"/>
<!-- Галочка (checkmark) -->
<path d="M5.086213,7.556L6.953213,9.867L10.420213,4.444" style="fill:none;stroke:#949494;stroke-width:1.41px;stroke-linecap:round;stroke-linejoin:round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -131,6 +131,7 @@ double getadhesionCoeff(const PrintObject* printObject)
} }
double adhesionCoeff = 1; double adhesionCoeff = 1;
for (const ModelVolume* modelVolume : objectVolumes) { for (const ModelVolume* modelVolume : objectVolumes) {
if (modelVolume->is_precise_seam()) continue; // non-printing helper geometry
for (auto iter = extrudersFirstLayer.begin(); iter != extrudersFirstLayer.end(); iter++) { for (auto iter = extrudersFirstLayer.begin(); iter != extrudersFirstLayer.end(); iter++) {
if (modelVolume->extruder_id() == *iter) { if (modelVolume->extruder_id() == *iter) {
if (Model::extruderParamsMap.find(modelVolume->extruder_id()) != Model::extruderParamsMap.end()) { if (Model::extruderParamsMap.find(modelVolume->extruder_id()) != Model::extruderParamsMap.end()) {
+2
View File
@@ -259,6 +259,8 @@ set(lisbslic3r_sources
GCode/RetractWhenCrossingPerimeters.hpp GCode/RetractWhenCrossingPerimeters.hpp
GCode/SeamPlacer.cpp GCode/SeamPlacer.cpp
GCode/SeamPlacer.hpp GCode/SeamPlacer.hpp
GCode/PreciseSeam.cpp
GCode/PreciseSeam.hpp
#GCodeSender.cpp #GCodeSender.cpp
#GCodeSender.hpp #GCodeSender.hpp
GCode/SmallAreaInfillFlowCompensator.cpp GCode/SmallAreaInfillFlowCompensator.cpp
+51 -5
View File
@@ -125,6 +125,10 @@ static constexpr const char* VOLUME_TYPE = "volume";
static constexpr const char* NAME_KEY = "name"; static constexpr const char* NAME_KEY = "name";
static constexpr const char* MODIFIER_KEY = "modifier"; static constexpr const char* MODIFIER_KEY = "modifier";
static constexpr const char* VOLUME_TYPE_KEY = "volume_type"; static constexpr const char* VOLUME_TYPE_KEY = "volume_type";
// Keep seam modes separate from the base type so older readers see a non-printing modifier.
static constexpr const char* PRECISE_SEAM_TYPE_KEY = "precise_seam_type";
// Preserve dormant settings without turning an older reader's modifier into an active override.
static constexpr char PRECISE_SEAM_CONFIG_PREFIX[] = "precise_seam_config:";
static constexpr const char* MATRIX_KEY = "matrix"; static constexpr const char* MATRIX_KEY = "matrix";
static constexpr const char* SOURCE_FILE_KEY = "source_file"; static constexpr const char* SOURCE_FILE_KEY = "source_file";
static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id"; static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id";
@@ -368,7 +372,14 @@ ModelVolumeType type_from_string(const std::string &s)
if (s == "ParameterModifier") return ModelVolumeType::PARAMETER_MODIFIER; if (s == "ParameterModifier") return ModelVolumeType::PARAMETER_MODIFIER;
if (s == "SupportEnforcer") return ModelVolumeType::SUPPORT_ENFORCER; if (s == "SupportEnforcer") return ModelVolumeType::SUPPORT_ENFORCER;
if (s == "SupportBlocker") return ModelVolumeType::SUPPORT_BLOCKER; if (s == "SupportBlocker") return ModelVolumeType::SUPPORT_BLOCKER;
// Default value if invalud type string received. // Precise Seam types (snake_case strings from ModelVolume::type_to_string)
if (s == "precise_seam_center") return ModelVolumeType::PRECISE_SEAM_CENTER;
if (s == "precise_seam_left") return ModelVolumeType::PRECISE_SEAM_LEFT;
if (s == "precise_seam_right") return ModelVolumeType::PRECISE_SEAM_RIGHT;
if (s == "precise_seam_enforced") return ModelVolumeType::PRECISE_SEAM_ENFORCED;
if (s == "precise_seam_blocked") return ModelVolumeType::PRECISE_SEAM_BLOCKED;
if (s == "precise_seam_neutral") return ModelVolumeType::PRECISE_SEAM_NEUTRAL;
// Default value if invalid type string received.
return ModelVolumeType::MODEL_PART; return ModelVolumeType::MODEL_PART;
} }
@@ -2035,6 +2046,7 @@ ModelVolumeType type_from_string(const std::string &s)
std::vector<std::string> valid_keys = { std::vector<std::string> valid_keys = {
"name", "name",
"volume_type", "volume_type",
PRECISE_SEAM_TYPE_KEY,
"matrix", "matrix",
"source_file", "source_file",
"source_object_id", "source_object_id",
@@ -2047,7 +2059,7 @@ ModelVolumeType type_from_string(const std::string &s)
}; };
auto itor = std::find(valid_keys.begin(), valid_keys.end(), key); auto itor = std::find(valid_keys.begin(), valid_keys.end(), key);
if (itor == valid_keys.end()) { if (itor == valid_keys.end() && !(type == VOLUME_TYPE && boost::starts_with(key, PRECISE_SEAM_CONFIG_PREFIX))) {
// do nothing if not valid keys // do nothing if not valid keys
return true; return true;
} }
@@ -2180,6 +2192,8 @@ ModelVolumeType type_from_string(const std::string &s)
volume->mmu_segmentation_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit();
volume->fuzzy_skin_facets.shrink_to_fit(); volume->fuzzy_skin_facets.shrink_to_fit();
// Apply the seam mode after all base-type metadata, regardless of XML key order.
ModelVolumeType precise_seam_type = ModelVolumeType::INVALID;
// apply the remaining volume's metadata // apply the remaining volume's metadata
for (const Metadata& metadata : volume_data.metadata) { for (const Metadata& metadata : volume_data.metadata) {
if (metadata.key == NAME_KEY) if (metadata.key == NAME_KEY)
@@ -2188,6 +2202,10 @@ ModelVolumeType type_from_string(const std::string &s)
volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
else if (metadata.key == VOLUME_TYPE_KEY) else if (metadata.key == VOLUME_TYPE_KEY)
volume->set_type(type_from_string(metadata.value)); volume->set_type(type_from_string(metadata.value));
else if (metadata.key == PRECISE_SEAM_TYPE_KEY)
precise_seam_type = ModelVolume::type_from_string(metadata.value);
else if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX))
continue; // Restore dormant settings only after the final volume type is known.
else if (metadata.key == SOURCE_FILE_KEY) else if (metadata.key == SOURCE_FILE_KEY)
volume->source.input_file = metadata.value; volume->source.input_file = metadata.value;
else if (metadata.key == SOURCE_OBJECT_ID_KEY) else if (metadata.key == SOURCE_OBJECT_ID_KEY)
@@ -2208,6 +2226,22 @@ ModelVolumeType type_from_string(const std::string &s)
volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions);
} }
// Missing or unknown seam modes retain the ordinary modifier fallback.
// Ignore seam metadata on other base types; legacy inline seam types still load above.
if (volume->is_modifier() && is_precise_seam(precise_seam_type))
volume->set_type(precise_seam_type);
// Unknown seam modes must remain inert modifiers, even when dormant settings are present.
if (volume->is_precise_seam()) {
for (const Metadata& metadata : volume_data.metadata) {
if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX)) {
const std::string key = metadata.key.substr(sizeof(PRECISE_SEAM_CONFIG_PREFIX) - 1);
if (!key.empty())
volume->config.set_deserialize(key, metadata.value, config_substitutions);
}
}
}
// this may happen for 3mf saved by 3rd part softwares // this may happen for 3mf saved by 3rd part softwares
if (volume->name.empty()) { if (volume->name.empty()) {
volume->name = object.name; volume->name = object.name;
@@ -3123,10 +3157,17 @@ ModelVolumeType type_from_string(const std::string &s)
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << NAME_KEY << "\" " << VALUE_ATTR << "=\"" << xml_escape(volume->name) << "\"/>\n"; stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << NAME_KEY << "\" " << VALUE_ATTR << "=\"" << xml_escape(volume->name) << "\"/>\n";
// stores volume's modifier field (legacy, to support old slicers) // stores volume's modifier field (legacy, to support old slicers)
if (volume->is_modifier()) // Readers with only the legacy flag still see helper geometry as a modifier.
if (volume->is_modifier() || volume->is_precise_seam())
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MODIFIER_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n"; stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MODIFIER_KEY << "\" " << VALUE_ATTR << "=\"1\"/>\n";
// stores volume's type (overrides the modifier field above) // This Prusa-format reader uses ParameterModifier, not Bambu's modifier_part.
// The base type overrides the legacy flag, so it must also be backward-compatible.
// Use the same spelling for ordinary modifiers, including a downgraded seam helper.
const bool store_as_modifier = volume->is_modifier() || volume->is_precise_seam();
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << VOLUME_TYPE_KEY << "\" " << stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << VOLUME_TYPE_KEY << "\" " <<
VALUE_ATTR << "=\"" << (store_as_modifier ? "ParameterModifier" : ModelVolume::type_to_string(volume->type())) << "\"/>\n";
if (volume->is_precise_seam())
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << PRECISE_SEAM_TYPE_KEY << "\" " <<
VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n"; VALUE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n";
// stores volume's local matrix // stores volume's local matrix
@@ -3162,7 +3203,12 @@ ModelVolumeType type_from_string(const std::string &s)
// stores volume's config data // stores volume's config data
for (const std::string& key : volume->config.keys()) { for (const std::string& key : volume->config.keys()) {
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; // Seam settings are inactive but must survive changing the helper back into a part/modifier.
const bool dormant = volume->is_precise_seam();
const std::string stored_key = dormant ? PRECISE_SEAM_CONFIG_PREFIX + key : key;
const std::string value = volume->config.opt_serialize(key);
// Config serialization is C-style, not XML: escape active settings too, including tabs.
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << stored_key << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(value) << "\"/>\n";
} }
// stores mesh's statistics // stores mesh's statistics
+38 -2
View File
@@ -353,6 +353,10 @@ static constexpr const char* PART_TYPE = "part";
static constexpr const char* NAME_KEY = "name"; static constexpr const char* NAME_KEY = "name";
static constexpr const char* VOLUME_TYPE_KEY = "volume_type"; static constexpr const char* VOLUME_TYPE_KEY = "volume_type";
static constexpr const char* PART_TYPE_KEY = "part_type"; static constexpr const char* PART_TYPE_KEY = "part_type";
// Keep seam modes separate from the base type so older readers see a non-printing modifier.
static constexpr const char* PRECISE_SEAM_TYPE_KEY = "precise_seam_type";
// Preserve dormant settings without turning an older reader's modifier into an active override.
static constexpr char PRECISE_SEAM_CONFIG_PREFIX[] = "precise_seam_config:";
static constexpr const char* MATRIX_KEY = "matrix"; static constexpr const char* MATRIX_KEY = "matrix";
static constexpr const char* SOURCE_FILE_KEY = "source_file"; static constexpr const char* SOURCE_FILE_KEY = "source_file";
static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id"; static constexpr const char* SOURCE_OBJECT_ID_KEY = "source_object_id";
@@ -5212,6 +5216,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (auto &tc = volume_data->text_configuration; tc.has_value()) if (auto &tc = volume_data->text_configuration; tc.has_value())
volume->text_configuration = std::move(tc); volume->text_configuration = std::move(tc);
// Apply the seam mode after all base-type metadata, regardless of XML key order.
ModelVolumeType precise_seam_type = ModelVolumeType::INVALID;
// apply the remaining volume's metadata // apply the remaining volume's metadata
for (const Metadata& metadata : volume_data->metadata) { for (const Metadata& metadata : volume_data->metadata) {
if (metadata.key == NAME_KEY) if (metadata.key == NAME_KEY)
@@ -5221,6 +5227,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//for old format //for old format
else if ((metadata.key == VOLUME_TYPE_KEY) || (metadata.key == PART_TYPE_KEY)) else if ((metadata.key == VOLUME_TYPE_KEY) || (metadata.key == PART_TYPE_KEY))
volume->set_type(ModelVolume::type_from_string(metadata.value)); volume->set_type(ModelVolume::type_from_string(metadata.value));
else if (metadata.key == PRECISE_SEAM_TYPE_KEY)
precise_seam_type = ModelVolume::type_from_string(metadata.value);
else if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX))
continue; // Restore dormant settings only after the final volume type is known.
else if (metadata.key == SOURCE_FILE_KEY) else if (metadata.key == SOURCE_FILE_KEY)
volume->source.input_file = metadata.value; volume->source.input_file = metadata.value;
else if (metadata.key == SOURCE_OBJECT_ID_KEY) else if (metadata.key == SOURCE_OBJECT_ID_KEY)
@@ -5243,6 +5253,22 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions); volume->config.set_deserialize(metadata.key, metadata.value, config_substitutions);
} }
// Missing or unknown seam modes retain the ordinary modifier fallback.
// Ignore seam metadata on other base types; legacy inline seam types still load above.
if (volume->is_modifier() && is_precise_seam(precise_seam_type))
volume->set_type(precise_seam_type);
// Unknown seam modes must remain inert modifiers, even when dormant settings are present.
if (volume->is_precise_seam()) {
for (const Metadata& metadata : volume_data->metadata) {
if (boost::starts_with(metadata.key, PRECISE_SEAM_CONFIG_PREFIX)) {
const std::string key = metadata.key.substr(sizeof(PRECISE_SEAM_CONFIG_PREFIX) - 1);
if (!key.empty())
volume->config.set_deserialize(key, metadata.value, config_substitutions);
}
}
}
// this may happen for 3mf saved by 3rd part softwares // this may happen for 3mf saved by 3rd part softwares
if (volume->name.empty()) { if (volume->name.empty()) {
volume->name = object.name; volume->name = object.name;
@@ -8002,7 +8028,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume_id = m_volume_paths.find(volume)->second.second; volume_id = m_volume_paths.find(volume)->second.second;
stream << ID_ATTR << "=\"" << volume_id << "\" "; stream << ID_ATTR << "=\"" << volume_id << "\" ";
stream << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\">\n"; // Older slicers must recognize the base type even when they ignore seam metadata.
const ModelVolumeType stored_type = volume->is_precise_seam() ? ModelVolumeType::PARAMETER_MODIFIER : volume->type();
stream << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(stored_type) << "\">\n";
if (volume->is_precise_seam())
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << PRECISE_SEAM_TYPE_KEY << "\" " << VALUE_ATTR << "=\"" <<
ModelVolume::type_to_string(volume->type()) << "\"/>\n";
//stream << " <" << PART_TAG << " " << ID_ATTR << "=\"" << it->second << "\" " << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\">\n"; //stream << " <" << PART_TAG << " " << ID_ATTR << "=\"" << it->second << "\" " << SUBTYPE_ATTR << "=\"" << ModelVolume::type_to_string(volume->type()) << "\">\n";
// stores volume's name // stores volume's name
@@ -8052,7 +8083,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// stores volume's config data // stores volume's config data
for (const std::string& key : volume->config.keys()) { for (const std::string& key : volume->config.keys()) {
stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << key << "\" " << VALUE_ATTR << "=\"" << volume->config.opt_serialize(key) << "\"/>\n"; // Seam settings are inactive but must survive changing the helper back into a part/modifier.
const bool dormant = volume->is_precise_seam();
const std::string stored_key = dormant ? PRECISE_SEAM_CONFIG_PREFIX + key : key;
const std::string value = volume->config.opt_serialize(key);
// Config serialization is C-style, not XML: escape active settings too, including tabs.
stream << " <" << METADATA_TAG << " "<< KEY_ATTR << "=\"" << stored_key << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(value) << "\"/>\n";
} }
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) { if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
#ifndef slic3r_PreciseSeam_hpp_
#define slic3r_PreciseSeam_hpp_
#include <atomic>
#include <optional>
#include <vector>
#include <unordered_map>
#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).
namespace Slic3r {
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>>;
// Warning flags set during Precise Seam processing (thread-safe)
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
};
// 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
};
// 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
Point right_point; // Coordinates of right (last) point of segment
size_t right_idx; // Perimeter vertex index for right_point
};
// 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
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
std::optional<Point> insert_strong_seam_point(
const std::vector<const ModelVolume*> &strong_volumes,
Polygon &polygon,
const Layer *layer,
const ModifierSlicesCache &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)
std::vector<WeakModifierSegment> collect_weak_modifier_segments(
const std::vector<const ModelVolume*> &weak_volumes,
Polygon &polygon,
const Layer *layer,
const ModifierSlicesCache &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
void apply_weak_modifiers_to_perimeter(
const std::vector<WeakModifierSegment> &weak_segments,
PrintObjectSeamData::LayerSeams &result,
const SeamPlacerImpl::Perimeter &perimeter,
bool &some_point_enforced);
// Restore precise seam positions that may have been modified by alignment
// Iterates through all perimeters and restores precise_seam_point positions
void restore_precise_seam_positions(std::vector<PrintObjectSeamData::LayerSeams> &layers);
} // namespace PreciseSeam
} // namespace Slic3r
#endif // slic3r_PreciseSeam_hpp_
+160 -42
View File
@@ -9,6 +9,7 @@
#include <random> #include <random>
#include <algorithm> #include <algorithm>
#include <queue> #include <queue>
#include <unordered_map>
#include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/KDTreeIndirect.hpp" #include "libslic3r/KDTreeIndirect.hpp"
@@ -19,10 +20,12 @@
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/Geometry/Curves.hpp" #include "libslic3r/Geometry/Curves.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/ShortEdgeCollapse.hpp" #include "libslic3r/ShortEdgeCollapse.hpp"
#include "libslic3r/TriangleSetSampling.hpp" #include "libslic3r/TriangleSetSampling.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "PreciseSeam.hpp"
//#define DEBUG_FILES //#define DEBUG_FILES
@@ -303,6 +306,16 @@ struct GlobalModelInfo {
AABBTreeIndirect::Tree<3, float> enforcers_tree; AABBTreeIndirect::Tree<3, float> enforcers_tree;
AABBTreeIndirect::Tree<3, float> blockers_tree; AABBTreeIndirect::Tree<3, float> blockers_tree;
// Precise Seam modifiers: strong modifiers (CENTER/LEFT/RIGHT) determine exact seam placement
std::vector<const ModelVolume*> precise_seam_strong_volumes;
// 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;
bool is_enforced(const Vec3f &position, float radius) const { bool is_enforced(const Vec3f &position, float radius) const {
if (enforcers.empty()) { if (enforcers.empty()) {
return false; return false;
@@ -403,7 +416,8 @@ struct GlobalModelInfo {
; ;
//Extract perimeter polygons of the given layer //Extract perimeter polygons of the given layer
Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerRegion*> &corresponding_regions_out) { Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerRegion*> &corresponding_regions_out,
bool has_precise_seam_modifiers) {
Polygons polygons; Polygons polygons;
for (const LayerRegion *layer_region : layer->regions()) { for (const LayerRegion *layer_region : layer->regions()) {
for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) {
@@ -440,6 +454,18 @@ Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerR
} }
} }
if (has_precise_seam_modifiers) {
// Extrusion loops repeat their start point; Polygon closes the contour implicitly.
// Normalize here for Precise Seam without changing ordinary seam candidates.
for (Polygon &polygon : polygons) {
// Adjacent extrusion paths share endpoints; zero-length edges would prevent refinement at their junctions.
// Remove only consecutive duplicates, preserving distinct visits to a self-touching contour point.
polygon.points.erase(std::unique(polygon.points.begin(), polygon.points.end()), polygon.points.end());
while (polygon.size() > 1 && polygon.points.front() == polygon.points.back())
polygon.points.pop_back();
}
}
if (polygons.empty()) { // If there are no perimeter polygons for whatever reason (disabled perimeters .. ) insert dummy point if (polygons.empty()) { // If there are no perimeter polygons for whatever reason (disabled perimeters .. ) insert dummy point
// it is easier than checking everywhere if the layer is not emtpy, no seam will be placed to this layer anyway // it is easier than checking everywhere if the layer is not emtpy, no seam will be placed to this layer anyway
polygons.emplace_back(Points{ { 0, 0 } }); polygons.emplace_back(Points{ { 0, 0 } });
@@ -449,17 +475,42 @@ Polygons extract_perimeter_polygons(const Layer *layer, std::vector<const LayerR
return polygons; return polygons;
} }
// Insert SeamCandidates created from perimeter polygons in to the result vector. // Build SeamCandidates for each vertex of the perimeter polygon and attach them to a shared Perimeter.
// Compute its type (Enfrocer,Blocker), angle, and position // For each vertex: computes position, angle, and type (Enforcer / Blocker / Neutral).
//each SeamCandidate also contains pointer to shared Perimeter structure representing the polygon // When Precise Seam modifiers are present: inserts strong seam point,
// if Custom Seam modifiers are present, oversamples the polygon if necessary to better fit user intentions // oversamples enforcer edges, applies weak modifiers, marks one enforced point as central for alignment.
void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const LayerRegion *region, void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const LayerRegion *region,
const GlobalModelInfo &global_model_info, PrintObjectSeamData::LayerSeams &result) { const GlobalModelInfo &global_model_info, PrintObjectSeamData::LayerSeams &result,
PreciseSeam::PreciseSeamWarnings* warnings = nullptr) {
if (orig_polygon.size() == 0) { if (orig_polygon.size() == 0) {
return; return;
} }
Polygon polygon = orig_polygon; Polygon polygon = orig_polygon;
bool was_clockwise = polygon.make_counter_clockwise(); bool was_clockwise = polygon.make_counter_clockwise();
// Process Precise Seam modifiers to find seam placement
const Layer* layer = region ? region->layer() : nullptr;
// Use pre-computed Precise Seam volumes from global_model_info (computed once in init)
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);
// 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; float angle_arm_len = region != nullptr ? region->flow(FlowRole::frExternalPerimeter).nozzle_diameter() : 0.5f;
std::vector<float> lengths { }; std::vector<float> lengths { };
@@ -528,10 +579,15 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
perimeter.end_index = result.points.size(); perimeter.end_index = result.points.size();
// Apply weak modifiers if no strong modifier was inserted
if (!inserted_seam_position.has_value() && !weak_segments.empty()) {
PreciseSeam::apply_weak_modifiers_to_perimeter(
weak_segments, result, perimeter, some_point_enforced);
}
if (some_point_enforced) { if (some_point_enforced) {
// We will patches of enforced points (patch: continuous section of enforced points), choose // Choose the continuous enforced patch with the most candidates, then select its middle
// the longest patch, and select the middle point or sharp point (depending on the angle) // candidate or a sharp corner. Patch length here is a point count, not geometric distance.
// this point will have high priority on this perimeter
size_t perimeter_size = perimeter.end_index - perimeter.start_index; size_t perimeter_size = perimeter.end_index - perimeter.start_index;
const auto next_index = [&](size_t idx) { const auto next_index = [&](size_t idx) {
return perimeter.start_index + Slic3r::next_idx_modulo(idx - perimeter.start_index, perimeter_size); return perimeter.start_index + Slic3r::next_idx_modulo(idx - perimeter.start_index, perimeter_size);
@@ -562,7 +618,9 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
std::pair<size_t, size_t> longest_patch { 0, 0 }; std::pair<size_t, size_t> longest_patch { 0, 0 };
auto patch_len = [perimeter_size](const std::pair<size_t, size_t> &start_end) { auto patch_len = [perimeter_size](const std::pair<size_t, size_t> &start_end) {
if (start_end.second < start_end.first) { if (start_end.second < start_end.first) {
return start_end.first + (perimeter_size - start_end.second); // Count [start, end) across the closing edge, independently of the contour's start.
// Subtract indices first: they are offsets in the layer, not local perimeter indices.
return perimeter_size - (start_end.first - start_end.second);
} else { } else {
return start_end.second - start_end.first; return start_end.second - start_end.first;
} }
@@ -595,30 +653,24 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
} }
} }
} // Apply precise seam point if it was inserted
if (inserted_seam_position.has_value()) {
// Get index of previous and next perimeter point of the layer. Because SeamCandidates of all polygons of the given layer // Set single point as Enforced, block all others
// are sequentially stored in the vector, each perimeter contains info about start and end index. These vales are used to for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) {
// deduce index of previous and next neigbour in the corresponding perimeter. if (result.points[i].position == inserted_seam_position.value()) {
std::pair<size_t, size_t> find_previous_and_next_perimeter_point(const std::vector<SeamCandidate> &perimeter_points, // Mark as the single enforced point with highest priority
size_t point_index) { result.points[i].type = EnforcedBlockedSeamPoint::Enforced;
const SeamCandidate &current = perimeter_points[point_index]; result.points[i].central_enforcer = true;
int prev = point_index - 1; //for majority of points, it is true that neighbours lie behind and in front of them in the vector perimeter.precise_seam_point = inserted_seam_position;
int next = point_index + 1; perimeter.precise_seam_index = i;
} else {
if (point_index == current.perimeter.start_index) { // Block all other points
// if point_index is equal to start, it means that the previous neighbour is at the end result.points[i].type = EnforcedBlockedSeamPoint::Blocked;
prev = current.perimeter.end_index; result.points[i].central_enforcer = false;
}
}
} }
if (point_index == current.perimeter.end_index - 1) {
// if point_index is equal to end, than next neighbour is at the start
next = current.perimeter.start_index;
}
assert(prev >= 0);
assert(next >= 0);
return {size_t(prev),size_t(next)};
} }
// Computes all global model info - transforms object, performs raycasting // Computes all global model info - transforms object, performs raycasting
@@ -636,7 +688,9 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
|| model_volume->type() == ModelVolumeType::NEGATIVE_VOLUME) { || model_volume->type() == ModelVolumeType::NEGATIVE_VOLUME) {
auto model_transformation = model_volume->get_matrix(); auto model_transformation = model_volume->get_matrix();
indexed_triangle_set model_its = model_volume->mesh().its; indexed_triangle_set model_its = model_volume->mesh().its;
// ORCA: Mirrored transforms flip winding, keep normals outward // ORCA fix (not related to Precise Seam, discovered during its development):
// Mirror transforms have negative determinant which flips triangle winding.
// fix_left_handed=true swaps indices to keep normals pointing outward.
its_transform(model_its, model_transformation, true); its_transform(model_its, model_transformation, true);
if (model_volume->type() == ModelVolumeType::MODEL_PART) { if (model_volume->type() == ModelVolumeType::MODEL_PART) {
its_merge(triangle_set, model_its); its_merge(triangle_set, model_its);
@@ -657,7 +711,10 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
size_t negative_volumes_start_index = triangle_set.indices.size(); size_t negative_volumes_start_index = triangle_set.indices.size();
its_merge(triangle_set, negative_volumes_set); its_merge(triangle_set, negative_volumes_set);
// ORCA: Mirroring flips normals, keep them outward for visibility sampling // ORCA fix (not related to Precise Seam, discovered during its development):
// Object-level transform may include mirroring (negative determinant),
// which inverts triangle winding. fix_left_handed=true corrects this
// so visibility ray sampling sees outward-facing normals.
its_transform(triangle_set, obj_transform, true); its_transform(triangle_set, obj_transform, true);
BOOST_LOG_TRIVIAL(debug) BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: decimate: end"; << "SeamPlacer: decimate: end";
@@ -719,12 +776,12 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
auto model_transformation = obj_transform * mv->get_matrix(); auto model_transformation = obj_transform * mv->get_matrix();
indexed_triangle_set enforcers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER); indexed_triangle_set enforcers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER);
// ORCA: Keep normals outward when mirroring seam enforcers // ORCA fix (not related to Precise Seam): fix winding for mirrored transforms
its_transform(enforcers, model_transformation, true); its_transform(enforcers, model_transformation, true);
its_merge(result.enforcers, enforcers); its_merge(result.enforcers, enforcers);
indexed_triangle_set blockers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::BLOCKER); indexed_triangle_set blockers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::BLOCKER);
// ORCA: Keep normals outward when mirroring seam blockers // ORCA fix (not related to Precise Seam): fix winding for mirrored transforms
its_transform(blockers, model_transformation, true); its_transform(blockers, model_transformation, true);
its_merge(result.blockers, blockers); its_merge(result.blockers, blockers);
} }
@@ -1011,13 +1068,14 @@ void pick_random_seam_point(const std::vector<SeamCandidate> &perimeter_points,
// Parallel process and extract each perimeter polygon of the given print object. // Parallel process and extract each perimeter polygon of the given print object.
// Gather SeamCandidates of each layer into vector and build KDtree over them // Gather SeamCandidates of each layer into vector and build KDtree over them
// Store results in the SeamPlacer variables m_seam_per_object // Store results in the SeamPlacer variables m_seam_per_object
void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info) { void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
PreciseSeam::PreciseSeamWarnings* warnings) {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
PrintObjectSeamData &seam_data = m_seam_per_object.emplace(po, PrintObjectSeamData { }).first->second; PrintObjectSeamData &seam_data = m_seam_per_object.emplace(po, PrintObjectSeamData { }).first->second;
seam_data.layers.resize(po->layer_count()); seam_data.layers.resize(po->layer_count());
tbb::parallel_for(tbb::blocked_range<size_t>(0, po->layers().size()), tbb::parallel_for(tbb::blocked_range<size_t>(0, po->layers().size()),
[po, &global_model_info, &seam_data] [po, &global_model_info, &seam_data, warnings]
(tbb::blocked_range<size_t> r) { (tbb::blocked_range<size_t> r) {
for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) {
PrintObjectSeamData::LayerSeams &layer_seams = seam_data.layers[layer_idx]; PrintObjectSeamData::LayerSeams &layer_seams = seam_data.layers[layer_idx];
@@ -1025,10 +1083,13 @@ void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerI
auto unscaled_z = layer->slice_z; auto unscaled_z = layer->slice_z;
std::vector<const LayerRegion*> regions; std::vector<const LayerRegion*> regions;
//NOTE corresponding region ptr may be null, if the layer has zero perimeters //NOTE corresponding region ptr may be null, if the layer has zero perimeters
Polygons polygons = extract_perimeter_polygons(layer, regions); const bool has_precise_seam_modifiers = !global_model_info.precise_seam_strong_volumes.empty() ||
!global_model_info.precise_seam_weak_volumes.empty();
Polygons polygons = extract_perimeter_polygons(layer, regions, has_precise_seam_modifiers);
for (size_t poly_index = 0; poly_index < polygons.size(); ++poly_index) { for (size_t poly_index = 0; poly_index < polygons.size(); ++poly_index) {
process_perimeter_polygon(polygons[poly_index], unscaled_z, process_perimeter_polygon(polygons[poly_index], unscaled_z,
regions[poly_index], global_model_info, layer_seams); regions[poly_index], global_model_info, layer_seams,
warnings);
} }
auto functor = SeamCandidateCoordinateFunctor { layer_seams.points }; auto functor = SeamCandidateCoordinateFunctor { layer_seams.points };
seam_data.layers[layer_idx].points_tree = seam_data.layers[layer_idx].points_tree =
@@ -1424,10 +1485,13 @@ void SeamPlacer::align_seam_points(const PrintObject *po, const SeamPlacerImpl::
} }
void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_canceled_func) { void SeamPlacer::init(Print &print, std::function<void(void)> throw_if_canceled_func) {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
m_seam_per_object.clear(); m_seam_per_object.clear();
// Warning flags for Precise Seam processing — shared across all objects
PreciseSeam::PreciseSeamWarnings precise_seam_warnings;
for (const PrintObject *po : print.objects()) { for (const PrintObject *po : print.objects()) {
throw_if_canceled_func(); throw_if_canceled_func();
SeamPosition configured_seam_preference = po->config().seam_position.value; SeamPosition configured_seam_preference = po->config().seam_position.value;
@@ -1436,14 +1500,29 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
{ {
GlobalModelInfo global_model_info { }; GlobalModelInfo global_model_info { };
gather_enforcers_blockers(global_model_info, po); gather_enforcers_blockers(global_model_info, po);
PreciseSeam::init_precise_seam_data(
global_model_info.precise_seam_strong_volumes,
global_model_info.precise_seam_weak_volumes,
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.
for (const ModelVolume* vol : global_model_info.precise_seam_strong_volumes)
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
for (const ModelVolume* vol : global_model_info.precise_seam_weak_volumes)
global_model_info.precise_seam_slices[vol] = po->slice_single_volume(vol);
throw_if_canceled_func(); throw_if_canceled_func();
if (configured_seam_preference == spAligned || configured_seam_preference == spNearest || configured_seam_preference == spAlignedBack) { if (configured_seam_preference == spAligned || configured_seam_preference == spNearest || configured_seam_preference == spAlignedBack) {
compute_global_occlusion(global_model_info, po, throw_if_canceled_func, configured_seam_preference); compute_global_occlusion(global_model_info, po, throw_if_canceled_func, configured_seam_preference);
} }
throw_if_canceled_func(); throw_if_canceled_func();
BOOST_LOG_TRIVIAL(debug) BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: gather_seam_candidates: start"; << "SeamPlacer: gather_seam_candidates: start";
gather_seam_candidates(po, global_model_info); gather_seam_candidates(po, global_model_info, &precise_seam_warnings);
BOOST_LOG_TRIVIAL(debug) BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: gather_seam_candidates: end"; << "SeamPlacer: gather_seam_candidates: end";
throw_if_canceled_func(); throw_if_canceled_func();
@@ -1491,10 +1570,49 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
<< "SeamPlacer: align_seam_points : end"; << "SeamPlacer: align_seam_points : end";
} }
// Restore precise seam positions that were potentially modified
if (m_seam_per_object[po].has_precise_seam_strong_volumes) {
PreciseSeam::restore_precise_seam_positions(m_seam_per_object[po].layers);
}
#ifdef DEBUG_FILES #ifdef DEBUG_FILES
debug_export_points(m_seam_per_object[po].layers, po->bounding_box(), comparator); debug_export_points(m_seam_per_object[po].layers, po->bounding_box(), comparator);
#endif #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.
{
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);
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 (!parts.empty()) {
// One line: the export warnings dialog shows only the first line of each warning.
std::string warning_text = _u8L("Precise Seam") + ": ";
for (size_t i = 0; i < parts.size(); ++i) {
if (i > 0) warning_text += "; ";
warning_text += parts[i];
}
warning_text += ". ";
warning_text += _u8L("Seam placement may differ from expected.");
print.active_step_add_warning(
PrintStateBase::WarningLevel::NON_CRITICAL,
warning_text,
PrintStateBase::SlicingPreciseSeamWarning);
}
}
} }
void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop,
+12 -3
View File
@@ -18,6 +18,7 @@
namespace Slic3r { namespace Slic3r {
class PrintObject; class PrintObject;
namespace PreciseSeam { struct PreciseSeamWarnings; }
class ExtrusionLoop; class ExtrusionLoop;
class Print; class Print;
class Layer; class Layer;
@@ -41,7 +42,7 @@ enum class EnforcedBlockedSeamPoint {
// struct representing single perimeter loop // struct representing single perimeter loop
struct Perimeter { struct Perimeter {
size_t start_index{}; size_t start_index{};
size_t end_index{}; //inclusive! size_t end_index{}; //exclusive (one-past-the-end)
size_t seam_index{}; size_t seam_index{};
float flow_width{}; float flow_width{};
@@ -50,6 +51,10 @@ struct Perimeter {
// Random position also uses this flexibility to set final seam point position // Random position also uses this flexibility to set final seam point position
bool finalized = false; bool finalized = false;
Vec3f final_seam_position = Vec3f::Zero(); Vec3f final_seam_position = Vec3f::Zero();
// Stores precise seam coordinates found by Precise Seam modifiers
std::optional<Vec3f> precise_seam_point;
size_t precise_seam_index{};
}; };
//Struct over which all processing of perimeters is done. For each perimeter point, its respective candidate is created, //Struct over which all processing of perimeters is done. For each perimeter point, its respective candidate is created,
@@ -102,6 +107,9 @@ struct PrintObjectSeamData
// Map of PrintObjects (PO) -> vector of layers of PO -> unique_ptr to KD // Map of PrintObjects (PO) -> vector of layers of PO -> unique_ptr to KD
// tree of all points of the given layer // tree of all points of the given layer
// Indicates presence of strong Precise Seam modifiers (CENTER/LEFT/RIGHT) for this object
bool has_precise_seam_strong_volumes = false;
void clear() void clear()
{ {
layers.clear(); layers.clear();
@@ -141,11 +149,12 @@ public:
//The following data structures hold all perimeter points for all PrintObject. //The following data structures hold all perimeter points for all PrintObject.
std::unordered_map<const PrintObject*, PrintObjectSeamData> m_seam_per_object; std::unordered_map<const PrintObject*, PrintObjectSeamData> m_seam_per_object;
void init(const Print &print, std::function<void(void)> throw_if_canceled_func); void init(Print &print, std::function<void(void)> throw_if_canceled_func);
void place_seam(const Layer *layer, ExtrusionLoop &loop, const Point &last_pos, float& overhang) const; void place_seam(const Layer *layer, ExtrusionLoop &loop, const Point &last_pos, float& overhang) const;
private: private:
void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info); void gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info,
PreciseSeam::PreciseSeamWarnings* warnings = nullptr);
void calculate_candidates_visibility(const PrintObject *po, void calculate_candidates_visibility(const PrintObject *po,
const SeamPlacerImpl::GlobalModelInfo &global_model_info); const SeamPlacerImpl::GlobalModelInfo &global_model_info);
void calculate_overhangs_and_layer_embedding(const PrintObject *po); void calculate_overhangs_and_layer_embedding(const PrintObject *po);
+52 -3
View File
@@ -23,6 +23,7 @@
#include "libslic3r/Geometry/ConvexHull.hpp" #include "libslic3r/Geometry/ConvexHull.hpp"
#include <algorithm>
#include <float.h> #include <float.h>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
@@ -1229,6 +1230,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
this->volumes.emplace_back(new ModelVolume(*model_volume)); this->volumes.emplace_back(new ModelVolume(*model_volume));
this->volumes.back()->set_model_object(this); this->volumes.back()->set_model_object(this);
} }
this->clear_instances(); this->clear_instances();
this->instances.reserve(rhs.instances.size()); this->instances.reserve(rhs.instances.size());
for (const ModelInstance *model_instance : rhs.instances) { for (const ModelInstance *model_instance : rhs.instances) {
@@ -1267,6 +1269,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
rhs.volumes.clear(); rhs.volumes.clear();
for (ModelVolume *model_volume : this->volumes) for (ModelVolume *model_volume : this->volumes)
model_volume->set_model_object(this); model_volume->set_model_object(this);
this->clear_instances(); this->clear_instances();
this->instances = std::move(rhs.instances); this->instances = std::move(rhs.instances);
rhs.instances.clear(); rhs.instances.clear();
@@ -1390,7 +1393,9 @@ ModelVolume* ModelObject::add_volume_with_shared_mesh(const ModelVolume &other,
void ModelObject::delete_volume(size_t idx) void ModelObject::delete_volume(size_t idx)
{ {
ModelVolumePtrs::iterator i = this->volumes.begin() + idx; ModelVolumePtrs::iterator i = this->volumes.begin() + idx;
delete *i; ModelVolume* volume_to_delete = *i;
delete volume_to_delete;
this->volumes.erase(i); this->volumes.erase(i);
if (this->volumes.size() == 1) if (this->volumes.size() == 1)
@@ -1449,6 +1454,20 @@ void ModelObject::sort_volumes(bool full_sort)
// sort volumes inside the object to order "Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. " // sort volumes inside the object to order "Model Part, Negative Volume, Modifier, Support Blocker and Support Enforcer. "
if (full_sort) if (full_sort)
std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) { std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) {
// Special handling for Precise Seam modifiers: group-based sorting with user order preservation
if (vl->is_precise_seam() && vr->is_precise_seam()) {
// Strong (center/left/right) always before weak (enforced/blocked/neutral)
bool vl_strong = vl->is_precise_seam_strong();
bool vr_strong = vr->is_precise_seam_strong();
if (vl_strong != vr_strong)
return vl_strong; // strong < weak → strong group appears first
// Within same group (both strong or both weak): preserve current order
// stable_sort will maintain relative positions when comparator returns false
return false;
}
// For non-Precise-Seam or mixed types: use standard enum-based ordering
return vl->type() < vr->type(); return vl->type() < vr->type();
}); });
// sort have to controll "place" of the support blockers/enforcers. But one of the model parts have to be on the first place. // sort have to controll "place" of the support blockers/enforcers. But one of the model parts have to be on the first place.
@@ -1456,10 +1475,19 @@ void ModelObject::sort_volumes(bool full_sort)
std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) { std::stable_sort(volumes.begin(), volumes.end(), [](ModelVolume* vl, ModelVolume* vr) {
ModelVolumeType vl_type = vl->type() > ModelVolumeType::PARAMETER_MODIFIER ? vl->type() : ModelVolumeType::PARAMETER_MODIFIER; ModelVolumeType vl_type = vl->type() > ModelVolumeType::PARAMETER_MODIFIER ? vl->type() : ModelVolumeType::PARAMETER_MODIFIER;
ModelVolumeType vr_type = vr->type() > ModelVolumeType::PARAMETER_MODIFIER ? vr->type() : ModelVolumeType::PARAMETER_MODIFIER; ModelVolumeType vr_type = vr->type() > ModelVolumeType::PARAMETER_MODIFIER ? vr->type() : ModelVolumeType::PARAMETER_MODIFIER;
// Apply same Precise Seam grouping logic for partial sort
if (vl->is_precise_seam() && vr->is_precise_seam()) {
bool vl_strong = vl->is_precise_seam_strong();
bool vr_strong = vr->is_precise_seam_strong();
if (vl_strong != vr_strong)
return vl_strong;
return false; // preserve order within same group
}
return vl_type < vr_type; return vl_type < vr_type;
}); });
} }
ModelInstance* ModelObject::add_instance() ModelInstance* ModelObject::add_instance()
{ {
ModelInstance* i = new ModelInstance(this); ModelInstance* i = new ModelInstance(this);
@@ -2615,7 +2643,8 @@ std::vector<int> ModelVolume::get_extruders() const
if (m_type == ModelVolumeType::INVALID if (m_type == ModelVolumeType::INVALID
|| m_type == ModelVolumeType::NEGATIVE_VOLUME || m_type == ModelVolumeType::NEGATIVE_VOLUME
|| m_type == ModelVolumeType::SUPPORT_BLOCKER || m_type == ModelVolumeType::SUPPORT_BLOCKER
|| m_type == ModelVolumeType::SUPPORT_ENFORCER) || m_type == ModelVolumeType::SUPPORT_ENFORCER
|| this->is_precise_seam()) // Precise Seam is non-printing helper geometry
return std::vector<int>(); return std::vector<int>();
if (mmu_segmentation_facets.timestamp() != mmuseg_ts) { if (mmu_segmentation_facets.timestamp() != mmuseg_ts) {
@@ -2819,6 +2848,19 @@ ModelVolumeType ModelVolume::type_from_string(const std::string &s)
return ModelVolumeType::SUPPORT_ENFORCER; return ModelVolumeType::SUPPORT_ENFORCER;
if (s == "support_blocker") if (s == "support_blocker")
return ModelVolumeType::SUPPORT_BLOCKER; return ModelVolumeType::SUPPORT_BLOCKER;
// Precise Seam types
if (s == "precise_seam_center")
return ModelVolumeType::PRECISE_SEAM_CENTER;
if (s == "precise_seam_left")
return ModelVolumeType::PRECISE_SEAM_LEFT;
if (s == "precise_seam_right")
return ModelVolumeType::PRECISE_SEAM_RIGHT;
if (s == "precise_seam_enforced")
return ModelVolumeType::PRECISE_SEAM_ENFORCED;
if (s == "precise_seam_blocked")
return ModelVolumeType::PRECISE_SEAM_BLOCKED;
if (s == "precise_seam_neutral")
return ModelVolumeType::PRECISE_SEAM_NEUTRAL;
//assert(s == "0"); //assert(s == "0");
// Default value if invalud type string received. // Default value if invalud type string received.
return ModelVolumeType::MODEL_PART; return ModelVolumeType::MODEL_PART;
@@ -2833,6 +2875,12 @@ std::string ModelVolume::type_to_string(const ModelVolumeType t)
case ModelVolumeType::PARAMETER_MODIFIER: return "modifier_part"; case ModelVolumeType::PARAMETER_MODIFIER: return "modifier_part";
case ModelVolumeType::SUPPORT_ENFORCER: return "support_enforcer"; case ModelVolumeType::SUPPORT_ENFORCER: return "support_enforcer";
case ModelVolumeType::SUPPORT_BLOCKER: return "support_blocker"; case ModelVolumeType::SUPPORT_BLOCKER: return "support_blocker";
case ModelVolumeType::PRECISE_SEAM_CENTER: return "precise_seam_center";
case ModelVolumeType::PRECISE_SEAM_LEFT: return "precise_seam_left";
case ModelVolumeType::PRECISE_SEAM_RIGHT: return "precise_seam_right";
case ModelVolumeType::PRECISE_SEAM_ENFORCED: return "precise_seam_enforced";
case ModelVolumeType::PRECISE_SEAM_BLOCKED: return "precise_seam_blocked";
case ModelVolumeType::PRECISE_SEAM_NEUTRAL: return "precise_seam_neutral";
default: default:
assert(false); assert(false);
return "normal_part"; return "normal_part";
@@ -3785,6 +3833,7 @@ bool model_volume_list_changed(const ModelObject &model_object_old, const ModelO
}); });
} }
template< typename TypeFilterFn, typename CompareFn> template< typename TypeFilterFn, typename CompareFn>
bool model_property_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, TypeFilterFn type_filter, CompareFn compare) bool model_property_changed(const ModelObject &model_object_old, const ModelObject &model_object_new, TypeFilterFn type_filter, CompareFn compare)
{ {
+23 -1
View File
@@ -347,8 +347,22 @@ enum class ModelVolumeType : int {
PARAMETER_MODIFIER, PARAMETER_MODIFIER,
SUPPORT_BLOCKER, SUPPORT_BLOCKER,
SUPPORT_ENFORCER, SUPPORT_ENFORCER,
// Precise seam modifiers (6 subtypes for seam placement control).
// Order is critical: strong types first, then weak. Range checks in is_precise_seam*() depend on it.
PRECISE_SEAM_CENTER,
PRECISE_SEAM_LEFT,
PRECISE_SEAM_RIGHT,
PRECISE_SEAM_ENFORCED,
PRECISE_SEAM_BLOCKED,
PRECISE_SEAM_NEUTRAL,
}; };
// 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; }
// A printable object, possibly having multiple print volumes (each with its own set of parameters and materials), // A printable object, possibly having multiple print volumes (each with its own set of parameters and materials),
// and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials. // and possibly having multiple modifier volumes, each modifier volume with its set of parameters and materials.
// Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed, // Each ModelObject may be instantiated mutliple times, each instance having different placement on the print bed,
@@ -917,6 +931,14 @@ public:
bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; } bool is_support_enforcer() const { return m_type == ModelVolumeType::SUPPORT_ENFORCER; }
bool is_support_blocker() const { return m_type == ModelVolumeType::SUPPORT_BLOCKER; } 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; } 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; }
// 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; }
// 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_text() const { return text_configuration.has_value(); } bool is_text() const { return text_configuration.has_value(); }
bool is_svg() const { return emboss_shape.has_value() && !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 bool is_the_only_one_part() const; // behave like an object
@@ -1041,7 +1063,7 @@ protected:
friend class SLAPrint; friend class SLAPrint;
friend class Model; friend class Model;
friend class ModelObject; friend class ModelObject;
friend void model_volume_list_update_supports(ModelObject& model_object_dst, const ModelObject& model_object_new); friend void model_volume_list_update_supports_and_seams(ModelObject& model_object_dst, const ModelObject& model_object_new);
// Copies IDs of both the ModelVolume and its config. // Copies IDs of both the ModelVolume and its config.
explicit ModelVolume(const ModelVolume &rhs) = default; explicit ModelVolume(const ModelVolume &rhs) = default;
+10 -1
View File
@@ -318,10 +318,13 @@ Points Polygon::concave_points(double angle_threshold) const
} }
// Projection of a point onto the polygon. // Projection of a point onto the polygon.
Point Polygon::point_projection(const Point &point) const Point Polygon::point_projection(const Point &point, size_t *edge_index) const
{ {
Point proj = point; Point proj = point;
double dmin = std::numeric_limits<double>::max(); 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()) { if (! this->points.empty()) {
for (size_t i = 0; i < this->points.size(); ++ i) { for (size_t i = 0; i < this->points.size(); ++ i) {
const Point &pt0 = this->points[i]; const Point &pt0 = this->points[i];
@@ -330,11 +333,15 @@ Point Polygon::point_projection(const Point &point) const
if (d < dmin) { if (d < dmin) {
dmin = d; dmin = d;
proj = pt0; proj = pt0;
if (edge_index)
*edge_index = i;
} }
d = (point - pt1).cast<double>().norm(); d = (point - pt1).cast<double>().norm();
if (d < dmin) { if (d < dmin) {
dmin = d; dmin = d;
proj = pt1; 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))); Vec2d v1(coordf_t(pt1(0) - pt0(0)), coordf_t(pt1(1) - pt0(1)));
coordf_t div = v1.squaredNorm(); coordf_t div = v1.squaredNorm();
@@ -347,6 +354,8 @@ Point Polygon::point_projection(const Point &point) const
if (d < dmin) { if (d < dmin) {
dmin = d; dmin = d;
proj = foot; proj = foot;
if (edge_index)
*edge_index = i;
} }
} }
} }
+3 -1
View File
@@ -85,7 +85,9 @@ public:
Points convex_points(double angle_threshold = 0.) const; Points convex_points(double angle_threshold = 0.) const;
Points concave_points(double angle_threshold = 0.) const; Points concave_points(double angle_threshold = 0.) const;
// Projection of a point onto the polygon. // Projection of a point onto the polygon.
Point point_projection(const Point &point) const; // 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;
std::vector<float> parameter_by_length() const; std::vector<float> parameter_by_length() const;
//BBS //BBS
+4
View File
@@ -470,6 +470,10 @@ public:
std::vector<Polygons> slice_support_volumes(const ModelVolumeType model_volume_type) const; 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_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); } 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}); }
// Helpers to project custom facets on slices // 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; void project_and_append_custom_facets(bool seam, EnforcerBlockerType type, std::vector<Polygons>& expolys, std::vector<std::pair<Vec3f,Vec3f>>* vertical_points=nullptr) const;
+50 -14
View File
@@ -8,12 +8,12 @@
namespace Slic3r { namespace Slic3r {
// Add or remove support modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new // Add or remove support and Precise Seam modifier ModelVolumes from model_object_dst to match the ModelVolumes of model_object_new
// in the exact order and with the same IDs. // in the exact order and with the same IDs.
// It is expected, that the model_object_dst already contains the non-support volumes of model_object_new in the correct order. // Other volume types must already match model_object_new in the correct order.
// Friend to ModelVolume to allow copying. // Friend to ModelVolume to allow copying.
// static is not accepted by gcc if declared as a friend of ModelObject. // static is not accepted by gcc if declared as a friend of ModelObject.
/* static */ void model_volume_list_update_supports(ModelObject &model_object_dst, const ModelObject &model_object_new) /* static */ void model_volume_list_update_supports_and_seams(ModelObject &model_object_dst, const ModelObject &model_object_new)
{ {
typedef std::pair<const ModelVolume*, bool> ModelVolumeWithStatus; typedef std::pair<const ModelVolume*, bool> ModelVolumeWithStatus;
std::vector<ModelVolumeWithStatus> old_volumes; std::vector<ModelVolumeWithStatus> old_volumes;
@@ -33,18 +33,22 @@ namespace Slic3r {
assert(! it->second); // not consumed yet assert(! it->second); // not consumed yet
it->second = true; it->second = true;
ModelVolume *model_volume_dst = const_cast<ModelVolume*>(it->first); ModelVolume *model_volume_dst = const_cast<ModelVolume*>(it->first);
// For support modifiers, the type may have been switched from blocker to enforcer and vice versa. // Type may switch within support family, within precise_seam family, or between them.
assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) || model_volume_dst->type() == model_volume_src->type()); assert((model_volume_dst->is_support_modifier() && model_volume_src->is_support_modifier()) ||
(model_volume_dst->is_precise_seam() && model_volume_src->is_precise_seam()) ||
(model_volume_dst->is_support_modifier() && model_volume_src->is_precise_seam()) ||
(model_volume_dst->is_precise_seam() && model_volume_src->is_support_modifier()) ||
model_volume_dst->type() == model_volume_src->type());
model_object_dst.volumes.emplace_back(model_volume_dst); model_object_dst.volumes.emplace_back(model_volume_dst);
if (model_volume_dst->is_support_modifier()) { if (model_volume_dst->is_support_modifier() || model_volume_dst->is_precise_seam()) {
// For support modifiers, the type may have been switched from blocker to enforcer and vice versa. // Type may have been switched within or between support/precise_seam families.
model_volume_dst->set_type(model_volume_src->type()); model_volume_dst->set_type(model_volume_src->type());
model_volume_dst->set_transformation(model_volume_src->get_transformation()); model_volume_dst->set_transformation(model_volume_src->get_transformation());
} }
assert(model_volume_dst->get_matrix().isApprox(model_volume_src->get_matrix())); assert(model_volume_dst->get_matrix().isApprox(model_volume_src->get_matrix()));
} else { } else {
// The volume was not found in the old list. Create a new copy. // The volume was not found in the old list. Create a new copy.
assert(model_volume_src->is_support_modifier()); assert(model_volume_src->is_support_modifier() || model_volume_src->is_precise_seam());
model_object_dst.volumes.emplace_back(new ModelVolume(*model_volume_src)); model_object_dst.volumes.emplace_back(new ModelVolume(*model_volume_src));
model_object_dst.volumes.back()->set_model_object(&model_object_dst); model_object_dst.volumes.back()->set_model_object(&model_object_dst);
} }
@@ -55,17 +59,20 @@ namespace Slic3r {
delete mv_with_status.first; delete mv_with_status.first;
} }
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type) // Copy configs of ModelVolumes matching type_filter predicate from src to dst.
// Mirrors the template pattern of model_volume_list_changed() in Model.cpp.
template<typename TypeFilterFn>
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, TypeFilterFn type_filter)
{ {
size_t i_src, i_dst; size_t i_src, i_dst;
for (i_src = 0, i_dst = 0; i_src < model_object_src.volumes.size() && i_dst < model_object_dst.volumes.size();) { for (i_src = 0, i_dst = 0; i_src < model_object_src.volumes.size() && i_dst < model_object_dst.volumes.size();) {
const ModelVolume &mv_src = *model_object_src.volumes[i_src]; const ModelVolume &mv_src = *model_object_src.volumes[i_src];
ModelVolume &mv_dst = *model_object_dst.volumes[i_dst]; ModelVolume &mv_dst = *model_object_dst.volumes[i_dst];
if (mv_src.type() != type) { if (! type_filter(mv_src.type())) {
++ i_src; ++ i_src;
continue; continue;
} }
if (mv_dst.type() != type) { if (! type_filter(mv_dst.type())) {
++ i_dst; ++ i_dst;
continue; continue;
} }
@@ -88,6 +95,20 @@ static inline void model_volume_list_copy_configs(ModelObject &model_object_dst,
} }
} }
// Convenience overload: single volume type.
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const ModelVolumeType type)
{
model_volume_list_copy_configs(model_object_dst, model_object_src, [type](const ModelVolumeType t) { return t == type; });
}
// Convenience overload: multiple volume types at once (e.g. all precise seam types).
static inline void model_volume_list_copy_configs(ModelObject &model_object_dst, const ModelObject &model_object_src, const std::initializer_list<ModelVolumeType> &types)
{
model_volume_list_copy_configs(model_object_dst, model_object_src, [&types](const ModelVolumeType t) {
return std::find(types.begin(), types.end(), t) != types.end();
});
}
static inline void layer_height_ranges_copy_configs(t_layer_config_ranges &lr_dst, const t_layer_config_ranges &lr_src) static inline void layer_height_ranges_copy_configs(t_layer_config_ranges &lr_dst, const t_layer_config_ranges &lr_src)
{ {
assert(lr_dst.size() == lr_src.size()); assert(lr_dst.size() == lr_src.size());
@@ -1618,6 +1639,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// 3) Synchronize ModelObjects & PrintObjects. // 3) Synchronize ModelObjects & PrintObjects.
const std::initializer_list<ModelVolumeType> solid_or_modifier_types { ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME, ModelVolumeType::PARAMETER_MODIFIER }; const std::initializer_list<ModelVolumeType> solid_or_modifier_types { ModelVolumeType::MODEL_PART, ModelVolumeType::NEGATIVE_VOLUME, ModelVolumeType::PARAMETER_MODIFIER };
const std::initializer_list<ModelVolumeType> precise_seam_types {
ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_LEFT,
ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_ENFORCED,
ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL
};
for (size_t idx_model_object = 0; idx_model_object < model.objects.size(); ++ idx_model_object) { for (size_t idx_model_object = 0; idx_model_object < model.objects.size(); ++ idx_model_object) {
ModelObject &model_object = *m_model.objects[idx_model_object]; ModelObject &model_object = *m_model.objects[idx_model_object];
ModelObjectStatus &model_object_status = const_cast<ModelObjectStatus&>(model_object_status_db.reuse(model_object)); ModelObjectStatus &model_object_status = const_cast<ModelObjectStatus&>(model_object_status_db.reuse(model_object));
@@ -1635,6 +1661,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
model_fuzzy_skin_data_changed(model_object, model_object_new); model_fuzzy_skin_data_changed(model_object, model_object_new);
bool supports_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_BLOCKER) || bool supports_differ = model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_BLOCKER) ||
model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_ENFORCER); model_volume_list_changed(model_object, model_object_new, ModelVolumeType::SUPPORT_ENFORCER);
bool precise_seam_differ = model_volume_list_changed(model_object, model_object_new, precise_seam_types);
bool layer_height_ranges_differ = ! layer_height_ranges_equal(model_object.layer_config_ranges, model_object_new.layer_config_ranges, model_object_new.layer_height_profile.empty()); bool layer_height_ranges_differ = ! layer_height_ranges_equal(model_object.layer_config_ranges, model_object_new.layer_config_ranges, model_object_new.layer_height_profile.empty());
bool model_origin_translation_differ = model_object.origin_translation != model_object_new.origin_translation; bool model_origin_translation_differ = model_object.origin_translation != model_object_new.origin_translation;
bool brim_points_differ = model_brim_points_data_changed(model_object, model_object_new); bool brim_points_differ = model_brim_points_data_changed(model_object, model_object_new);
@@ -1677,13 +1704,20 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Invalidate just the supports step. // Invalidate just the supports step.
for (const PrintObjectStatus &print_object_status : print_objects_range) for (const PrintObjectStatus &print_object_status : print_objects_range)
update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial)); update_apply_status(print_object_status.print_object->invalidate_step(posSupportMaterial));
if (supports_differ) {
// Copy just the support volumes.
model_volume_list_update_supports(model_object, model_object_new);
} }
if (precise_seam_differ) {
// First stop background processing before shuffling or deleting the ModelVolumes in the ModelObject's list.
this->call_cancel_callback();
update_apply_status(false);
// Invalidate seam placement (affects G-code export).
update_apply_status(this->invalidate_step(psGCodeExport));
} else if (model_custom_seam_data_changed(model_object, model_object_new)) { } else if (model_custom_seam_data_changed(model_object, model_object_new)) {
update_apply_status(this->invalidate_step(psGCodeExport)); update_apply_status(this->invalidate_step(psGCodeExport));
} }
// Synchronize both families once, after cancellation and all affected-step invalidations.
// This also handles type changes between supports and Precise Seam before copying configs below.
if (supports_differ || precise_seam_differ)
model_volume_list_update_supports_and_seams(model_object, model_object_new);
if (brim_points_differ) { if (brim_points_differ) {
model_object.brim_points = model_object_new.brim_points; model_object.brim_points = model_object_new.brim_points;
update_apply_status(this->invalidate_all_steps()); update_apply_status(this->invalidate_all_steps());
@@ -1708,6 +1742,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
//FIXME What to do with m_material_id? //FIXME What to do with m_material_id?
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::MODEL_PART); model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::MODEL_PART);
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::PARAMETER_MODIFIER); model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, ModelVolumeType::PARAMETER_MODIFIER);
// Synchronize Precise Seam modifier volumes
model_volume_list_copy_configs(model_object /* dst */, model_object_new /* src */, precise_seam_types);
layer_height_ranges_copy_configs(model_object.layer_config_ranges /* dst */, model_object_new.layer_config_ranges /* src */); layer_height_ranges_copy_configs(model_object.layer_config_ranges /* dst */, model_object_new.layer_config_ranges /* src */);
// Copy the ModelObject name, input_file and instances. The instances will be compared against PrintObject instances in the next step. // Copy the ModelObject name, input_file and instances. The instances will be compared against PrintObject instances in the next step.
model_object.name = model_object_new.name; model_object.name = model_object_new.name;
+2 -1
View File
@@ -62,7 +62,8 @@ public:
SlicingReplaceInitEmptyLayers, SlicingReplaceInitEmptyLayers,
SlicingNeedSupportOn, SlicingNeedSupportOn,
SlicingEmptyGcodeLayers, SlicingEmptyGcodeLayers,
SlicingGcodeOverlap SlicingGcodeOverlap,
SlicingPreciseSeamWarning
}; };
typedef size_t TimeStamp; typedef size_t TimeStamp;
+14 -8
View File
@@ -1551,12 +1551,19 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_
std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const
{ {
auto it_volume = this->model_object()->volumes.begin(); // Supports merge every matching volume; Precise Seam calls the shared slicer one volume at a time.
auto it_volume_end = this->model_object()->volumes.end(); std::vector<const ModelVolume*> volumes;
for (; it_volume != it_volume_end && (*it_volume)->type() != model_volume_type; ++ it_volume) ; 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
{
std::vector<Polygons> slices; std::vector<Polygons> slices;
if (it_volume != it_volume_end) { if (!volumes.empty()) {
// Found at least a single support volume of model_volume_type. // Share layer heights, transforms and cancellation handling across the selected volumes.
std::vector<float> zs = zs_from_layers(this->layers()); std::vector<float> zs = zs_from_layers(this->layers());
std::vector<char> merge_layers; std::vector<char> merge_layers;
bool merge = false; bool merge = false;
@@ -1564,9 +1571,8 @@ std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType m
auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); }); auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); });
MeshSlicingParamsEx params; MeshSlicingParamsEx params;
params.trafo = this->trafo_centered(); params.trafo = this->trafo_centered();
for (; it_volume != it_volume_end; ++ it_volume) for (const ModelVolume *volume : volumes) {
if ((*it_volume)->type() == model_volume_type) { std::vector<ExPolygons> slices2 = slice_volume(*volume, zs, params, throw_on_cancel_callback);
std::vector<ExPolygons> slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback);
if (slices.empty()) { if (slices.empty()) {
slices.reserve(slices2.size()); slices.reserve(slices2.size());
for (ExPolygons &src : slices2) for (ExPolygons &src : slices2)
+30
View File
@@ -162,6 +162,14 @@ 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}; ColorRGBA GLVolume::MODEL_HIDDEN_COL = {0.f, 0.f, 0.f, 0.3f};
// Precise Seam modifier colors
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
ColorRGBA GLVolume::PRECISE_SEAM_ENFORCED_COL = {0.412f, 0.820f, 0.412f, 0.6f}; // 69D169 - green
ColorRGBA GLVolume::PRECISE_SEAM_NEUTRAL_COL = {0.655f, 0.655f, 0.655f, 0.6f}; // A7A7A7 - gray
ColorRGBA GLVolume::PRECISE_SEAM_BLOCKED_COL = {0.820f, 0.412f, 0.412f, 0.6f}; // D16969 - red
std::array<ColorRGBA, 5> GLVolume::MODEL_COLOR = { { std::array<ColorRGBA, 5> GLVolume::MODEL_COLOR = { {
{ 1.0f, 1.0f, 0.0f, 1.f }, { 1.0f, 1.0f, 0.0f, 1.f },
{ 1.0f, 0.5f, 0.5f, 1.f }, { 1.0f, 0.5f, 0.5f, 1.f },
@@ -363,6 +371,28 @@ ColorRGBA color_from_model_volume(const ModelVolume& model_volume)
ColorRGBA color; ColorRGBA color;
if (model_volume.is_negative_volume()) if (model_volume.is_negative_volume())
return GLVolume::MODEL_NEGTIVE_COL; return GLVolume::MODEL_NEGTIVE_COL;
else if (model_volume.is_precise_seam()) {
// Return color based on Precise Seam subtype.
// Exhaustive switch (no default) so -Wswitch flags any future PRECISE_SEAM_* additions.
switch (model_volume.type()) {
case ModelVolumeType::PRECISE_SEAM_CENTER: return GLVolume::PRECISE_SEAM_CENTER_COL;
case ModelVolumeType::PRECISE_SEAM_LEFT: return GLVolume::PRECISE_SEAM_LEFT_COL;
case ModelVolumeType::PRECISE_SEAM_RIGHT: return GLVolume::PRECISE_SEAM_RIGHT_COL;
case ModelVolumeType::PRECISE_SEAM_ENFORCED: return GLVolume::PRECISE_SEAM_ENFORCED_COL;
case ModelVolumeType::PRECISE_SEAM_NEUTRAL: return GLVolume::PRECISE_SEAM_NEUTRAL_COL;
case ModelVolumeType::PRECISE_SEAM_BLOCKED: return GLVolume::PRECISE_SEAM_BLOCKED_COL;
// Non-seam types are unreachable due to the outer is_precise_seam() guard;
// listed explicitly so this switch stays exhaustive over ModelVolumeType.
case ModelVolumeType::INVALID:
case ModelVolumeType::MODEL_PART:
case ModelVolumeType::NEGATIVE_VOLUME:
case ModelVolumeType::PARAMETER_MODIFIER:
case ModelVolumeType::SUPPORT_BLOCKER:
case ModelVolumeType::SUPPORT_ENFORCER:
break;
}
return GLVolume::MODEL_MIDIFIER_COL; // unreachable fallback
}
else if (model_volume.is_modifier()) else if (model_volume.is_modifier())
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT #if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
return GLVolume::MODEL_MIDIFIER_COL; return GLVolume::MODEL_MIDIFIER_COL;
+7
View File
@@ -93,6 +93,13 @@ public:
static ColorRGBA SUPPORT_ENFORCER_COL; static ColorRGBA SUPPORT_ENFORCER_COL;
static ColorRGBA SUPPORT_BLOCKER_COL; static ColorRGBA SUPPORT_BLOCKER_COL;
static ColorRGBA MODEL_HIDDEN_COL; static ColorRGBA MODEL_HIDDEN_COL;
// Precise Seam modifier colors
static ColorRGBA PRECISE_SEAM_CENTER_COL;
static ColorRGBA PRECISE_SEAM_LEFT_COL;
static ColorRGBA PRECISE_SEAM_RIGHT_COL;
static ColorRGBA PRECISE_SEAM_ENFORCED_COL;
static ColorRGBA PRECISE_SEAM_NEUTRAL_COL;
static ColorRGBA PRECISE_SEAM_BLOCKED_COL;
static void update_render_colors(); static void update_render_colors();
static void load_render_colors(); static void load_render_colors();
+166 -5
View File
@@ -336,13 +336,20 @@ wxBitmap SettingsFactory::get_category_bitmap(const std::string& category_name,
//------------------------------------- //-------------------------------------
// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important // Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important
static const constexpr std::array<std::pair<const char *, const char *>, 5> ADD_VOLUME_MENU_ITEMS = {{ static const constexpr std::array<std::pair<const char *, const char *>, 11> ADD_VOLUME_MENU_ITEMS = {{
// menu_item Name menu_item bitmap name // menu_item Name menu_item bitmap name
{L("Add Part"), "menu_add_part" }, // ~ModelVolumeType::MODEL_PART {L("Add Part"), "menu_add_part" }, // ~ModelVolumeType::MODEL_PART
{L("Add Negative Part"), "menu_add_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME {L("Add Negative Part"), "menu_add_negative" }, // ~ModelVolumeType::NEGATIVE_VOLUME
{L("Add Modifier"), "menu_add_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER {L("Add Modifier"), "menu_add_modifier"}, // ~ModelVolumeType::PARAMETER_MODIFIER
{L("Add Support Blocker"), "menu_support_blocker"}, // ~ModelVolumeType::SUPPORT_BLOCKER {L("Add Support Blocker"), "menu_support_blocker"}, // ~ModelVolumeType::SUPPORT_BLOCKER
{L("Add Support Enforcer"), "menu_support_enforcer"}, // ~ModelVolumeType::SUPPORT_ENFORCER {L("Add Support Enforcer"), "menu_support_enforcer"}, // ~ModelVolumeType::SUPPORT_ENFORCER
// Precise Seam modifiers (all 6 subtypes - only first one shown in menu, others used for tree icons)
{L("Add Precise Seam"), "menu_precise_seam_center"}, // ~ModelVolumeType::PRECISE_SEAM_CENTER
{L("Add Precise Seam"), "menu_precise_seam_left"}, // ~ModelVolumeType::PRECISE_SEAM_LEFT
{L("Add Precise Seam"), "menu_precise_seam_right"}, // ~ModelVolumeType::PRECISE_SEAM_RIGHT
{L("Add Precise Seam"), "menu_precise_seam_enforced"}, // ~ModelVolumeType::PRECISE_SEAM_ENFORCED
{L("Add Precise Seam"), "menu_precise_seam_blocked"}, // ~ModelVolumeType::PRECISE_SEAM_BLOCKED
{L("Add Precise Seam"), "menu_precise_seam_neutral"}, // ~ModelVolumeType::PRECISE_SEAM_NEUTRAL
}}; }};
// Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important // Note: id accords to type of the sub-object (adding volume), so sequence of the menu items is important
@@ -740,10 +747,20 @@ void MenuFactory::append_menu_items_add_volume(wxMenu* menu)
for (size_t type = 0; type < ADD_VOLUME_MENU_ITEMS.size(); type++) for (size_t type = 0; type < ADD_VOLUME_MENU_ITEMS.size(); type++)
{ {
// Skip Precise Seam subtypes except the first one (CENTER) - they are only used for tree icons
if (type >= int(ModelVolumeType::PRECISE_SEAM_LEFT) &&
type <= int(ModelVolumeType::PRECISE_SEAM_NEUTRAL))
continue;
auto& item = ADD_VOLUME_MENU_ITEMS[type]; auto& item = ADD_VOLUME_MENU_ITEMS[type];
// Use special icon for "Add Precise Seam" menu (different from tree icon)
std::string icon_name = item.second;
if (type == int(ModelVolumeType::PRECISE_SEAM_CENTER))
icon_name = "menu_precise_seam_add";
wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type)); wxMenu* sub_menu = append_submenu_add_generic(menu, ModelVolumeType(type));
append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", item.second, append_submenu(menu, sub_menu, wxID_ANY, _(item.first), "", icon_name,
[]() { return obj_list()->is_instance_or_object_selected(); }, m_parent); []() { return obj_list()->is_instance_or_object_selected(); }, m_parent);
} }
@@ -847,7 +864,12 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
{ ModelVolumeType::NEGATIVE_VOLUME, _L("Negative Part") }, { ModelVolumeType::NEGATIVE_VOLUME, _L("Negative Part") },
{ ModelVolumeType::PARAMETER_MODIFIER, _L("Modifier") }, { ModelVolumeType::PARAMETER_MODIFIER, _L("Modifier") },
{ ModelVolumeType::SUPPORT_BLOCKER, _L("Support Blocker") }, { ModelVolumeType::SUPPORT_BLOCKER, _L("Support Blocker") },
{ ModelVolumeType::SUPPORT_ENFORCER, _L("Support Enforcer") } { ModelVolumeType::SUPPORT_ENFORCER, _L("Support Enforcer") },
// Single "Precise Seam" entry that maps to PRECISE_SEAM_CENTER as the default subtype.
// set_volume_type() preserves the existing subtype (LEFT/RIGHT/etc.) for volumes that
// are already Precise Seam; subtype picking is done via the separate
// "Precise Seam Type" submenu (see append_menu_item_precise_seam_submenu).
{ ModelVolumeType::PRECISE_SEAM_CENTER, _L("Precise Seam") }
}; };
for (const auto& info : types) { for (const auto& info : types) {
@@ -863,7 +885,13 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
obj_list()->GetSelections(sels); obj_list()->GetSelections(sels);
for (auto item : sels) { for (auto item : sels) {
ModelVolumeType vol_type = model->GetVolumeType(item); ModelVolumeType vol_type = model->GetVolumeType(item);
if (vol_type == type) { // The "Precise Seam" entry represents all six PS subtypes, so any subtype
// among selected volumes counts as a match (keeps the checkbox ticked when
// user has PS_LEFT/RIGHT/etc. selected, not only plain CENTER).
const bool match = (type == ModelVolumeType::PRECISE_SEAM_CENTER)
? is_precise_seam(vol_type)
: (vol_type == type);
if (match) {
has_type = true; has_type = true;
break; break;
} }
@@ -871,6 +899,7 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
evt.Check(has_type); evt.Check(has_type);
// ORCA Fix crash caused by SVG/TEXT volumes cant be Support Enforcer/Blocker type // ORCA Fix crash caused by SVG/TEXT volumes cant be Support Enforcer/Blocker type
// The same applies to Precise Seam subtypes.
for (auto item : sels) { for (auto item : sels) {
if (model->GetItemType(item) == itVolume){ if (model->GetItemType(item) == itVolume){
auto vol_idx = model->GetVolumeIdByItem(item); auto vol_idx = model->GetVolumeIdByItem(item);
@@ -881,7 +910,8 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
auto vol = (*objs)[obj_idx]->volumes[vol_idx]; auto vol = (*objs)[obj_idx]->volumes[vol_idx];
// disable Support Enforcer/Blocker if selection contains svg or text // disable Support Enforcer/Blocker if selection contains svg or text
if (vol != nullptr && (vol->is_svg() || vol->is_text()) && (type == ModelVolumeType::SUPPORT_BLOCKER || type == ModelVolumeType::SUPPORT_ENFORCER)){ // (same applies to Precise Seam)
if (vol != nullptr && (vol->is_svg() || vol->is_text()) && (type == ModelVolumeType::SUPPORT_BLOCKER || type == ModelVolumeType::SUPPORT_ENFORCER || is_precise_seam(type))){
evt.Enable(false); evt.Enable(false);
break; break;
} }
@@ -1309,6 +1339,131 @@ void MenuFactory::append_menu_items_mirror(wxMenu* menu)
[]() { return plater()->can_mirror(); }, m_parent); []() { return plater()->can_mirror(); }, m_parent);
} }
void MenuFactory::append_menu_item_precise_seam_submenu(wxMenu* menu)
{
wxString submenu_name = _L("Precise Seam Type");
// Remove existing submenu if present (menu is rebuilt on every right-click)
const int menu_item_id = menu->FindItem(submenu_name);
if (menu_item_id != wxNOT_FOUND)
menu->Destroy(menu_item_id);
// --- Precondition: ALL selected volumes must be Precise Seam ---
// Mixed selections (PS + non-PS) are ambiguous for subtype switching: applying a subtype
// would implicitly convert the non-PS volumes to PS, which is not what the user expects
// from a subtype picker. For mixed selections the user should first use
// "Change type → Precise Seam" to unify them, then come back to this submenu.
wxDataViewItemArray sels;
obj_list()->GetSelections(sels);
if (sels.IsEmpty())
return;
// A right-click on a volume in the 3D view routes through Plater::show_context_menu
// → ObjectList::update_selections(), which intentionally preserves a pre-existing
// settings-row selection alongside the clicked volume. As a result GetSelections()
// may return settings-row items that are children of a volume. Treat each settings
// row as selecting its parent volume (same pattern used by set_volume_type()
// internally). Without this resolution, GetVolumeType(settings_row) returns INVALID
// and the submenu would hide even when a valid PS volume is in the selection.
//
// Precomputing the resolved PS types once here also lets the checkmark loop below
// display checks correctly for selections that contain settings rows.
std::vector<ModelVolumeType> selected_ps_types;
selected_ps_types.reserve(sels.size());
auto* model = obj_list()->GetModel();
for (const auto& sel_item : sels) {
wxDataViewItem vol_item = sel_item;
const ItemType type_mask = model->GetItemType(sel_item);
if (!(type_mask & itVolume)) {
// Only settings rows whose parent is a volume map to "selecting that volume".
// Any other non-volume item (object, instance, etc.) means the selection is
// not purely PS-volume-based — hide the submenu.
if ((type_mask & itSettings) && (model->GetItemType(model->GetParent(sel_item)) & itVolume))
vol_item = model->GetParent(sel_item);
else
return;
}
const ModelVolumeType vol_type = model->GetVolumeType(vol_item);
if (!is_precise_seam(vol_type))
return;
selected_ps_types.push_back(vol_type);
}
// Match the label used by append_menu_item_change_type, including its translation.
int insert_pos = wxNOT_FOUND;
int change_type_id = menu->FindItem(_L("Change Type"));
if (change_type_id != wxNOT_FOUND) {
for (size_t i = 0; i < menu->GetMenuItemCount(); i++) {
wxMenuItem* item = menu->FindItemByPosition(i);
if (item && item->GetId() == change_type_id) {
insert_pos = i + 1; // insert directly after the Change Type entry
break;
}
}
}
// --- Build the subtype submenu ---
wxMenu* ps_menu = new wxMenu();
// Array of all 6 Precise Seam subtypes with labels.
// "Seam ..." prefix disambiguates from other i18n contexts (extruder Left/Right, "Center on bed", etc.)
static const std::array<std::pair<const char*, ModelVolumeType>, 6> PS_TYPES = {{
{L("Seam Center"), ModelVolumeType::PRECISE_SEAM_CENTER},
{L("Seam Left"), ModelVolumeType::PRECISE_SEAM_LEFT},
{L("Seam Right"), ModelVolumeType::PRECISE_SEAM_RIGHT},
{L("Seam Enforced"), ModelVolumeType::PRECISE_SEAM_ENFORCED},
{L("Seam Blocked"), ModelVolumeType::PRECISE_SEAM_BLOCKED},
{L("Seam Neutral"), ModelVolumeType::PRECISE_SEAM_NEUTRAL},
}};
for (const auto& ps_type : PS_TYPES) {
wxString label = _(ps_type.first);
// Check-items (not radio): radio groups in wxWidgets auto-select the first item when
// no item is explicitly checked, which misleads the user into seeing "Seam Center"
// as the active subtype for mixed-subtype selections. With check-items we can leave
// every item unchecked in that case, and the menu matches the pattern of the
// neighbouring "Change type" submenu (see append_menu_item_change_type).
//
// Handler: delegate to set_volume_type() with preserve_ps_subtype=false. The user
// explicitly picked a subtype here, so "Seam Center" must set every selected PS
// volume to CENTER verbatim — preservation (the default for "Change type") would
// keep existing subtypes and make CENTER unreachable on mixed-subtype selections.
// set_volume_type() already handles multi-select iteration, the last-solid-part
// guard, and the PS group-change reordering via move_volume_to_end().
wxMenuItem* item = append_menu_check_item(ps_menu, wxID_ANY, label, "",
[ps_type](wxCommandEvent&) {
obj_list()->set_volume_type(ps_type.second, /*preserve_ps_subtype=*/false);
},
ps_menu);
// Tick every subtype that is present in the current selection. Same pattern as the
// neighbouring "Change type" submenu (where selecting [Part + Modifier] ticks both
// "Part" and "Modifier" boxes). For homogeneous selections exactly one checkbox is
// ticked; for mixed subtype selections (e.g. PS_LEFT + PS_RIGHT) both "Seam Left"
// and "Seam Right" appear checked, so the user sees at a glance which subtypes are
// currently in the selection.
//
// Uses the precomputed selected_ps_types (which resolved settings rows to their
// parent volumes); iterating sels directly would miss subtypes for selections that
// arrive via settings rows from right-click-in-3D-view.
const bool is_present = std::find(selected_ps_types.begin(), selected_ps_types.end(),
ps_type.second) != selected_ps_types.end();
if (is_present && item)
item->Check(true);
}
// Add submenu to parent menu
append_submenu(menu, ps_menu, wxID_ANY,
submenu_name,
_L("Choose precise seam subtype"),
"menu_precise_seam_type",
[]() { return true; },
m_parent,
insert_pos);
}
void MenuFactory::append_menu_item_edit_text(wxMenu *menu) void MenuFactory::append_menu_item_edit_text(wxMenu *menu)
{ {
wxString name = _L("Edit text"); wxString name = _L("Edit text");
@@ -1578,6 +1733,7 @@ void MenuFactory::create_part_menu()
menu->AppendSeparator(); menu->AppendSeparator();
append_menu_item_change_type(menu); append_menu_item_change_type(menu);
append_menu_item_precise_seam_submenu(menu);
append_menu_items_mirror(&m_part_menu); append_menu_items_mirror(&m_part_menu);
append_menu_item(&m_part_menu, wxID_ANY, _L("Split"), _L("Split the selected object into multiple parts"), append_menu_item(&m_part_menu, wxID_ANY, _L("Split"), _L("Split the selected object into multiple parts"),
[](wxCommandEvent&) { plater()->split_volume(); }, "split_parts", nullptr, [](wxCommandEvent&) { plater()->split_volume(); }, "split_parts", nullptr,
@@ -1875,6 +2031,7 @@ wxMenu* MenuFactory::part_menu()
{ {
append_menu_items_convert_unit(&m_part_menu); append_menu_items_convert_unit(&m_part_menu);
append_menu_item_change_filament(&m_part_menu); append_menu_item_change_filament(&m_part_menu);
append_menu_item_precise_seam_submenu(&m_part_menu);
append_menu_item_per_object_settings(&m_part_menu); append_menu_item_per_object_settings(&m_part_menu);
return &m_part_menu; return &m_part_menu;
} }
@@ -1989,6 +2146,10 @@ wxMenu* MenuFactory::multi_selection_menu()
append_menu_item_per_object_process(menu); append_menu_item_per_object_process(menu);
menu->AppendSeparator(); menu->AppendSeparator();
append_menu_item_change_type(menu); append_menu_item_change_type(menu);
// Subtype picker for Precise Seam — shown only when all selected volumes are PS.
// Must be paired with Change Type here (as in single-volume part_menu), otherwise
// the user cannot switch PS subtypes (LEFT/RIGHT/etc.) on multi-selection.
append_menu_item_precise_seam_submenu(menu);
append_menu_item_change_filament(menu); append_menu_item_change_filament(menu);
} }
return menu; return menu;
+1
View File
@@ -174,6 +174,7 @@ private:
void append_menu_item_merge_to_single_object(wxMenu* menu); void append_menu_item_merge_to_single_object(wxMenu* menu);
void append_menu_item_merge_parts_to_single_part(wxMenu *menu); void append_menu_item_merge_parts_to_single_part(wxMenu *menu);
void append_menu_items_mirror(wxMenu *menu); void append_menu_items_mirror(wxMenu *menu);
void append_menu_item_precise_seam_submenu(wxMenu* menu);
void append_menu_item_invalidate_cut_info(wxMenu *menu); void append_menu_item_invalidate_cut_info(wxMenu *menu);
void append_menu_item_edit_text(wxMenu *menu); void append_menu_item_edit_text(wxMenu *menu);
void append_menu_item_edit_svg(wxMenu *menu); void append_menu_item_edit_svg(wxMenu *menu);
+131 -10
View File
@@ -1944,6 +1944,12 @@ bool ObjectList::can_drop(const wxDataViewItem& item, int& src_obj_id, int& src_
if (dragged_item_v_type == item_v_type && dragged_item_v_type != ModelVolumeType::MODEL_PART) if (dragged_item_v_type == item_v_type && dragged_item_v_type != ModelVolumeType::MODEL_PART)
return true; return true;
// Use tree item types: hidden cut connectors make tree indices differ from volumes indices.
if (is_precise_seam(dragged_item_v_type) && is_precise_seam(item_v_type))
// Allow reordering only within the strong or weak modifier group.
return is_precise_seam_strong(dragged_item_v_type) == is_precise_seam_strong(item_v_type);
if ((dragged_item_v_type != item_v_type) || // we can't reorder volumes outside of types if ((dragged_item_v_type != item_v_type) || // we can't reorder volumes outside of types
item_v_type >= ModelVolumeType::SUPPORT_BLOCKER) // support blockers/enforcers can't change its place item_v_type >= ModelVolumeType::SUPPORT_BLOCKER) // support blockers/enforcers can't change its place
return false; return false;
@@ -2033,10 +2039,43 @@ void ObjectList::OnDrop(wxDataViewEvent &event)
int to_volume_id = m_objects_model->GetVolumeIdByItem(item); int to_volume_id = m_objects_model->GetVolumeIdByItem(item);
int delta = to_volume_id < from_volume_id ? -1 : 1; int delta = to_volume_id < from_volume_id ? -1 : 1;
auto &volumes = (*m_objects)[m_dragged_data.obj_idx()]->volumes; const int obj_idx = m_dragged_data.obj_idx();
// Object-indexed UI maps may be stale after another object is removed or reordered.
if (obj_idx < 0 || size_t(obj_idx) >= m_objects->size()) {
event.Veto();
m_dragged_data.clear();
return;
}
const ModelObject *object = (*m_objects)[obj_idx];
auto &volumes = (*m_objects)[obj_idx]->volumes;
std::vector<size_t> visible_volume_indices;
visible_volume_indices.reserve(volumes.size());
for (size_t idx = 0; idx < volumes.size(); ++idx)
// Match add_volumes_to_object_in_list: only connectors of cut objects are hidden.
if (!(object->is_cut() && volumes[idx]->is_cut_connector()))
visible_volume_indices.push_back(idx);
int cnt = 0; // Validate the entire move before any swap; these checks must also protect Release builds.
for (int id = from_volume_id; cnt < abs(from_volume_id - to_volume_id); id += delta, cnt++) std::swap(volumes[id], volumes[id + delta]); if (from_volume_id < 0 || to_volume_id < 0 ||
size_t(from_volume_id) >= visible_volume_indices.size() || size_t(to_volume_id) >= visible_volume_indices.size()) {
event.Veto();
m_dragged_data.clear();
return;
}
// Move through visible slots only, keeping hidden cut connectors at their original indices.
// The local mapping stays valid because no hidden volume changes slots during the move.
for (int id = from_volume_id; id != to_volume_id; id += delta) {
const size_t current_idx = visible_volume_indices[id];
const size_t next_idx = visible_volume_indices[id + delta];
std::swap(volumes[current_idx], volumes[next_idx]);
}
// Later selection/filament handlers use this cache; repair it for the moved object too.
auto &ui_to_model = m_objects_model->get_ui_and_3d_volume_map()[obj_idx];
ui_to_model.clear();
for (size_t idx = 0; idx < visible_volume_indices.size(); ++idx)
ui_to_model[int(idx)] = int(visible_volume_indices[idx]);
select_item(m_objects_model->ReorganizeChildren(from_volume_id, to_volume_id, m_objects_model->GetParent(item))); select_item(m_objects_model->ReorganizeChildren(from_volume_id, to_volume_id, m_objects_model->GetParent(item)));
@@ -3855,7 +3894,8 @@ wxDataViewItem ObjectList::add_settings_item(wxDataViewItem parent_item, const D
const bool is_layer_settings = m_objects_model->GetItemType(parent_item) == itLayer; const bool is_layer_settings = m_objects_model->GetItemType(parent_item) == itLayer;
if (!is_object_settings) { if (!is_object_settings) {
ModelVolumeType volume_type = m_objects_model->GetVolumeType(parent_item); ModelVolumeType volume_type = m_objects_model->GetVolumeType(parent_item);
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER) // Precise Seam is non-printing helper geometry — no per-volume settings
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER || is_precise_seam(volume_type))
return ret; return ret;
} }
@@ -5712,7 +5752,6 @@ void ObjectList::change_part_type()
return; return;
} }
#endif #endif
ModelVolumeType ObjectList::get_selected_volume_type() ModelVolumeType ObjectList::get_selected_volume_type()
{ {
ModelVolume* volume = get_selected_model_volume(); ModelVolume* volume = get_selected_model_volume();
@@ -5721,13 +5760,49 @@ ModelVolumeType ObjectList::get_selected_volume_type()
return ModelVolumeType::INVALID; return ModelVolumeType::INVALID;
} }
void ObjectList::set_volume_type(ModelVolumeType new_type) // ---------------- Helpers for Precise Seam group-aware type changes ----------------
// Used by set_volume_type() and the "Precise Seam Type" subtype submenu handler.
// Detects whether a type change crosses a Precise Seam "group boundary" that requires
// manual repositioning inside ModelObject::volumes[]:
// - between any non-PS type and any PS subtype, or
// - between strong PS (CENTER/LEFT/RIGHT) and weak PS (ENFORCED/BLOCKED/NEUTRAL).
// sort_volumes() is stable and groups strong PS before weak PS. When a volume crosses
// a group, moving it to the end of volumes[] lets the subsequent stable sort place it
// at the end of its new group. Without this, a strong→weak transition would leave the
// volume stuck in the strong segment and break the modifier-application order.
static bool precise_seam_group_changed(ModelVolumeType old_type, ModelVolumeType new_type)
{
const bool old_is_ps = is_precise_seam(old_type);
const bool new_is_ps = is_precise_seam(new_type);
if (old_is_ps != new_is_ps)
return true; // non-PS ↔ PS transition
if (!old_is_ps)
return false; // both non-PS — regular enum ordering is enough
return is_precise_seam_strong(old_type) != is_precise_seam_strong(new_type);
}
// Moves a volume to the end of ModelObject::volumes[]. Combined with stable sort_volumes(),
// this places it at the end of its new group while preserving relative order of others.
static void move_volume_to_end(ModelObject* obj, ModelVolume* volume)
{
if (obj == nullptr || volume == nullptr)
return;
auto it = std::find(obj->volumes.begin(), obj->volumes.end(), volume);
if (it != obj->volumes.end()) {
obj->volumes.erase(it);
obj->volumes.push_back(volume);
}
}
void ObjectList::set_volume_type(ModelVolumeType new_type, bool preserve_ps_subtype)
{ {
struct VolumeSelection { struct VolumeSelection {
int object_idx; int object_idx;
ModelVolume* volume; ModelVolume* volume;
}; };
// --- Collect selected volumes from the object tree, falling back to the 3D canvas ---
std::vector<VolumeSelection> volumes; std::vector<VolumeSelection> volumes;
auto add_volume = [&volumes](int obj_idx, ModelVolume* volume) { auto add_volume = [&volumes](int obj_idx, ModelVolume* volume) {
if (volume == nullptr) if (volume == nullptr)
@@ -5792,25 +5867,49 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
// and historically crashed (originally fixed in the now-disabled change_part_type() by hiding // and historically crashed (originally fixed in the now-disabled change_part_type() by hiding
// Support entries in the old choice dialog; the UI-side guard for the current submenu lives // Support entries in the old choice dialog; the UI-side guard for the current submenu lives
// in MenuFactory::append_menu_item_change_type, see #13120). // in MenuFactory::append_menu_item_change_type, see #13120).
// The same rationale applies to any Precise Seam subtype.
// This block must never be reachable under a healthy UI; if it ever logs, the UI guard has // This block must never be reachable under a healthy UI; if it ever logs, the UI guard has
// been bypassed (new entry point, refactor, plugin, etc.) and should be investigated. // been bypassed (new entry point, refactor, plugin, etc.) and should be investigated.
if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER) { if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER
|| is_precise_seam(new_type)) {
const bool has_text_or_svg = std::any_of(volumes.begin(), volumes.end(), const bool has_text_or_svg = std::any_of(volumes.begin(), volumes.end(),
[](const VolumeSelection& sel) { return sel.volume->is_svg() || sel.volume->is_text(); }); [](const VolumeSelection& sel) { return sel.volume->is_svg() || sel.volume->is_text(); });
if (has_text_or_svg) { if (has_text_or_svg) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ BOOST_LOG_TRIVIAL(error) << __FUNCTION__
<< ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER on SVG/text volume; " << ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER or Precise Seam on SVG/text volume; "
<< "UI guard should have prevented this -- possible regression in the Change Type menu"; << "UI guard should have prevented this -- possible regression in the Change Type menu";
return; return;
} }
} }
// --- Subtype preservation for the generic "Precise Seam" entry ---
// The Change Type submenu uses PRECISE_SEAM_CENTER as the single "Precise Seam" entry
// and calls with preserve_ps_subtype=true: volumes that are already Precise Seam keep
// their subtype (LEFT/RIGHT/etc.); only non-PS volumes get converted to the default
// CENTER subtype.
//
// The "Precise Seam Type" subtype submenu calls with preserve_ps_subtype=false: the
// user explicitly picked CENTER and all selected PS volumes must be set to CENTER
// verbatim (otherwise CENTER would become unreachable for mixed-subtype selections,
// since preservation would keep every volume at its current subtype and any_diff would
// be false — the reason for this two-parameter split).
auto effective_new_type = [new_type, preserve_ps_subtype](const ModelVolume* v) -> ModelVolumeType {
if (preserve_ps_subtype && new_type == ModelVolumeType::PRECISE_SEAM_CENTER && v->is_precise_seam())
return v->type();
return new_type;
};
// --- Any-change check using the effective target type ---
const bool any_diff = std::any_of(volumes.begin(), volumes.end(), const bool any_diff = std::any_of(volumes.begin(), volumes.end(),
[new_type](const VolumeSelection& sel) { return sel.volume->type() != new_type; }); [&effective_new_type](const VolumeSelection& sel) {
return sel.volume->type() != effective_new_type(sel.volume);
});
if (!any_diff) if (!any_diff)
return; return;
// --- Last-solid-part guard (pre-existing behavior) ---
// When converting away from MODEL_PART, ensure at least one solid part remains per object.
if (new_type != ModelVolumeType::MODEL_PART) { if (new_type != ModelVolumeType::MODEL_PART) {
std::map<int, int> total_part_cnt; std::map<int, int> total_part_cnt;
std::map<int, int> selected_part_cnt; std::map<int, int> selected_part_cnt;
@@ -5838,14 +5937,36 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
take_snapshot(_u8L("Change part type")); take_snapshot(_u8L("Change part type"));
// --- Apply type changes with Precise Seam group-aware repositioning ---
// Note: `changed_volumes` / `touched_objects` track every processed volume, including
// subtype-preservation no-ops. Tracking no-ops is intentional — it ensures such
// volumes remain selected after the post-apply rebuild. Without that, a cross-object
// mixed selection like [Part, PS_LEFT] clicking "Change type → Precise Seam" would
// silently deselect the preserved PS_LEFT: only the Part's object would enter
// touched_objects, and the final SetSelections(new_selection) would drop PS_LEFT.
std::set<const ModelVolume*> changed_volumes; std::set<const ModelVolume*> changed_volumes;
std::set<int> touched_objects; std::set<int> touched_objects;
for (const auto& sel : volumes) { for (const auto& sel : volumes) {
sel.volume->set_type(new_type); const ModelVolumeType target = effective_new_type(sel.volume);
const ModelVolumeType old_type = sel.volume->type();
// Record the volume for reselection before the no-op short-circuit (see comment above).
changed_volumes.insert(sel.volume); changed_volumes.insert(sel.volume);
touched_objects.insert(sel.object_idx); touched_objects.insert(sel.object_idx);
if (old_type == target)
continue; // subtype-preservation no-op — nothing to write/move
sel.volume->set_type(target);
// If the change crosses a PS group boundary, move the volume to the end of volumes[]
// so the stable sort in reorder_volumes_and_get_selection() places it at the end of
// its new group (see precise_seam_group_changed() for details).
if (precise_seam_group_changed(old_type, target))
move_volume_to_end((*m_objects)[sel.object_idx], sel.volume);
} }
// --- Rebuild selection to follow the changed volumes ---
wxDataViewItemArray new_selection; wxDataViewItemArray new_selection;
for (int obj_idx : touched_objects) { for (int obj_idx : touched_objects) {
wxDataViewItemArray sel_items = reorder_volumes_and_get_selection(obj_idx, [&changed_volumes](const ModelVolume* volume) { wxDataViewItemArray sel_items = reorder_volumes_and_get_selection(obj_idx, [&changed_volumes](const ModelVolume* volume) {
+7 -1
View File
@@ -88,6 +88,7 @@ struct MeshErrorsInfo
class ObjectList : public wxDataViewCtrl class ObjectList : public wxDataViewCtrl
{ {
public: public:
enum SELECTION_MODE enum SELECTION_MODE
{ {
smUndef = 0, smUndef = 0,
@@ -430,7 +431,12 @@ public:
#if 0 // ORCA: disabled alongside definition in GUI_ObjectList.cpp (see #if 0 block there) #if 0 // ORCA: disabled alongside definition in GUI_ObjectList.cpp (see #if 0 block there)
void change_part_type(); void change_part_type();
#endif #endif
void set_volume_type(ModelVolumeType new_type); // preserve_ps_subtype = true: when new_type is PRECISE_SEAM_CENTER, volumes that are
// already Precise Seam keep their existing subtype (LEFT/RIGHT/etc.). Used by the
// generic "Change type → Precise Seam" entry where CENTER is a default fallback.
// preserve_ps_subtype = false: no preservation — target type is applied verbatim. Used
// by the "Precise Seam Type" subtype picker where the user explicitly wants CENTER.
void set_volume_type(ModelVolumeType new_type, bool preserve_ps_subtype = true);
ModelVolumeType get_selected_volume_type(); ModelVolumeType get_selected_volume_type();
void last_volume_is_deleted(const int obj_idx); void last_volume_is_deleted(const int obj_idx);
+4 -2
View File
@@ -348,7 +348,8 @@ void GridCellFilamentsRenderer::Draw(wxGrid &grid, wxGridCellAttr &attr, wxDC &d
if ((grid_row->model_volume_type != ModelVolumeType::NEGATIVE_VOLUME) && \ if ((grid_row->model_volume_type != ModelVolumeType::NEGATIVE_VOLUME) && \
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_BLOCKER) && \ (grid_row->model_volume_type != ModelVolumeType::SUPPORT_BLOCKER) && \
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_ENFORCER) && \ (grid_row->model_volume_type != ModelVolumeType::SUPPORT_ENFORCER) && \
(grid_row->model_volume_type != ModelVolumeType::PARAMETER_MODIFIER)) { (grid_row->model_volume_type != ModelVolumeType::PARAMETER_MODIFIER) && \
!is_precise_seam(grid_row->model_volume_type)) { // Precise Seam is non-printing helper geometry
dc.DrawBitmap(*bitmap, wxPoint(rect.x + offset_x, rect.y + offset_y)); dc.DrawBitmap(*bitmap, wxPoint(rect.x + offset_x, rect.y + offset_y));
} }
else if (grid_row->model_volume_type == ModelVolumeType::PARAMETER_MODIFIER){ else if (grid_row->model_volume_type == ModelVolumeType::PARAMETER_MODIFIER){
@@ -3011,7 +3012,8 @@ void ObjectTablePanel::load_data()
if (col == ObjectGridTable::col_filaments) { if (col == ObjectGridTable::col_filaments) {
if ((grid_row->model_volume_type != ModelVolumeType::NEGATIVE_VOLUME) && \ if ((grid_row->model_volume_type != ModelVolumeType::NEGATIVE_VOLUME) && \
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_BLOCKER) && \ (grid_row->model_volume_type != ModelVolumeType::SUPPORT_BLOCKER) && \
(grid_row->model_volume_type != ModelVolumeType::SUPPORT_ENFORCER)) { (grid_row->model_volume_type != ModelVolumeType::SUPPORT_ENFORCER) && \
!is_precise_seam(grid_row->model_volume_type)) { // Precise Seam is non-printing helper geometry
GridCellFilamentsEditor* filament_editor = new GridCellFilamentsEditor(grid_col->choices, false, &m_color_bitmaps); GridCellFilamentsEditor* filament_editor = new GridCellFilamentsEditor(grid_col->choices, false, &m_color_bitmaps);
m_object_grid->SetCellEditor(row, col, filament_editor); m_object_grid->SetCellEditor(row, col, filament_editor);
m_object_grid->SetCellRenderer(row, col, new GridCellFilamentsRenderer()); m_object_grid->SetCellRenderer(row, col, new GridCellFilamentsRenderer());
+1 -1
View File
@@ -2000,7 +2000,7 @@ void GLGizmoEmboss::draw_model_type()
if ((is_volume_move_inside || is_volume_move_outside)) if ((is_volume_move_inside || is_volume_move_outside))
process(); process();
// inspiration in ObjectList::change_part_type() // inspiration in ObjectList::set_volume_type()
// how to view correct side panel with objects // how to view correct side panel with objects
ObjectList *obj_list = app.obj_list(); ObjectList *obj_list = app.obj_list();
wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection( wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(
+1 -1
View File
@@ -2052,7 +2052,7 @@ void GLGizmoSVG::draw_model_type()
if ((is_volume_move_inside || is_volume_move_outside)) if ((is_volume_move_inside || is_volume_move_outside))
process(); process();
// inspiration in ObjectList::change_part_type() // inspiration in ObjectList::set_volume_type()
// how to view correct side panel with objects // how to view correct side panel with objects
ObjectList *obj_list = app.obj_list(); ObjectList *obj_list = app.obj_list();
wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection( wxDataViewItemArray sel = obj_list->reorder_volumes_and_get_selection(
+4 -1
View File
@@ -11,6 +11,7 @@
#include "MainFrame.hpp" #include "MainFrame.hpp"
#include "Tab.hpp" #include "Tab.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/PrintBase.hpp"
#include "format.hpp" #include "format.hpp"
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
@@ -3084,7 +3085,9 @@ bool NotificationManager::push_notification_data(std::unique_ptr<NotificationMan
bool retval = false; bool retval = false;
if (this->activate_existing(notification.get())) { if (this->activate_existing(notification.get())) {
if (m_initialized && m_imgui_ready) { if (m_initialized && m_imgui_ready) {
if (notification->get_type() == NotificationType::SlicingWarning) { // Precise Seam already aggregates all causes; replace it on repeated warning events.
if (notification->get_type() == NotificationType::SlicingWarning &&
notification->get_data().sub_msg_id != PrintStateBase::SlicingPreciseSeamWarning) {
m_pop_notifications.back()->append(notification->get_data().ori_text); m_pop_notifications.back()->append(notification->get_data().ori_text);
} else { } else {
m_pop_notifications.back()->update(notification->get_data()); m_pop_notifications.back()->update(notification->get_data());
+2 -1
View File
@@ -546,7 +546,8 @@ void ObjectDataViewModel::UpdateBitmapForNode(ObjectDataViewModelNode *node)
{ {
bool is_volume_node = node->GetType() & itVolume; bool is_volume_node = node->GetType() & itVolume;
int vol_type = static_cast<int>(node->GetVolumeType()); int vol_type = static_cast<int>(node->GetVolumeType());
is_volume_node &= (vol_type >= int(ModelVolumeType::MODEL_PART) && vol_type <= int(ModelVolumeType::SUPPORT_ENFORCER)); // Extended range to include Precise Seam modifier types
is_volume_node &= (vol_type >= int(ModelVolumeType::MODEL_PART) && vol_type <= int(ModelVolumeType::PRECISE_SEAM_NEUTRAL));
if (!node->has_warning_icon() && !node->has_lock()) { if (!node->has_warning_icon() && !node->has_lock()) {
node->SetBitmap(is_volume_node ? ( node->SetBitmap(is_volume_node ? (
+1 -1
View File
@@ -67,7 +67,7 @@ bool Selection::Clipboard::is_sla_compliant() const
return false; return false;
for (const ModelVolume* v : o->volumes) { for (const ModelVolume* v : o->volumes) {
if (v->is_modifier()) if (v->is_modifier() || v->is_precise_seam()) // Precise Seam not supported in SLA
return false; return false;
} }
} }
+12 -1
View File
@@ -24,7 +24,14 @@ void host_bindings::register_model(py::module_& host)
.value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME) .value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME)
.value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER) .value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER)
.value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER) .value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER)
.value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER); .value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER)
// Expose every seam mode returned by ModelVolume::type() to plugins.
.value("PreciseSeamCenter", ModelVolumeType::PRECISE_SEAM_CENTER)
.value("PreciseSeamLeft", ModelVolumeType::PRECISE_SEAM_LEFT)
.value("PreciseSeamRight", ModelVolumeType::PRECISE_SEAM_RIGHT)
.value("PreciseSeamEnforced", ModelVolumeType::PRECISE_SEAM_ENFORCED)
.value("PreciseSeamBlocked", ModelVolumeType::PRECISE_SEAM_BLOCKED)
.value("PreciseSeamNeutral", ModelVolumeType::PRECISE_SEAM_NEUTRAL);
py::class_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(host, "ModelVolume") py::class_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(host, "ModelVolume")
.def("id", [](const ModelVolume& volume) { return volume.id().id; }) .def("id", [](const ModelVolume& volume) { return volume.id().id; })
@@ -36,6 +43,10 @@ void host_bindings::register_model(py::module_& host)
.def("is_support_enforcer", &ModelVolume::is_support_enforcer) .def("is_support_enforcer", &ModelVolume::is_support_enforcer)
.def("is_support_blocker", &ModelVolume::is_support_blocker) .def("is_support_blocker", &ModelVolume::is_support_blocker)
.def("is_support_modifier", &ModelVolume::is_support_modifier) .def("is_support_modifier", &ModelVolume::is_support_modifier)
// Let plugins identify seam helper geometry without relying on enum ordering.
.def("is_precise_seam", &ModelVolume::is_precise_seam)
.def("is_precise_seam_strong", &ModelVolume::is_precise_seam_strong)
.def("is_precise_seam_weak", &ModelVolume::is_precise_seam_weak)
// Extruder ID is 1-based for FFF, -1 for SLA or support volumes. // Extruder ID is 1-based for FFF, -1 for SLA or support volumes.
.def("extruder_id", &ModelVolume::extruder_id) .def("extruder_id", &ModelVolume::extruder_id)
.def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); }) .def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); })
+3
View File
@@ -14,9 +14,12 @@ add_executable(${_TEST_NAME}_tests
test_model.cpp test_model.cpp
test_multifilament.cpp test_multifilament.cpp
test_perimeters.cpp test_perimeters.cpp
# Seam geometry and pipeline regressions share the existing Print fixtures.
test_precise_seam.cpp
test_print.cpp test_print.cpp
test_printobject.cpp test_printobject.cpp
test_mixed_filament.cpp test_mixed_filament.cpp
test_seam_placer.cpp
test_skirt_brim.cpp test_skirt_brim.cpp
test_slicing_pipeline_hook.cpp test_slicing_pipeline_hook.cpp
test_support_material.cpp test_support_material.cpp
+279
View File
@@ -0,0 +1,279 @@
#include <catch2/catch_all.hpp>
#include "test_helpers.hpp"
#include "libslic3r/GCode/PreciseSeam.hpp"
#include <algorithm>
using namespace Slic3r;
namespace {
Point mm(double x, double y) { return Point(scale_(x), scale_(y)); }
Polygon rectangle(double x0, double y0, double x1, double y1)
{
// Counterclockwise contours match the modifier-slice cache contract.
return Polygon(Points{mm(x0, y0), mm(x1, y0), mm(x1, y1), mm(x0, y1)});
}
struct SeamFixture {
Model model;
Print print;
Model modifiers;
Layer *layer = nullptr;
PreciseSeam::ModifierSlicesCache cache;
SeamFixture()
{
// Only the layer/PrintObject context is needed; clipping uses explicit cached slices below.
Test::init_print({Test::cube(20)}, print, model, {{"raft_layers", "0"}});
REQUIRE(print.objects().size() == 1);
PrintObject *object = print.get_object(0);
layer = object->add_layer(int(object->slicing_parameters().raft_layers()), 0.2, 0.2, 0.1);
modifiers.add_object();
}
const ModelVolume *add(ModelVolumeType type, Polygons slices)
{
// These volumes own cache keys; mesh slicing is deliberately outside this geometry fixture.
ModelVolume *volume = modifiers.objects.front()->add_volume(Test::cube(1));
volume->set_type(type);
cache.emplace(volume, std::vector<Polygons>{std::move(slices)});
return volume;
}
};
void check_square_boundary(const Polygon &polygon)
{
// Wrong insertion edges can retrace a side without changing area: check length as well.
CHECK_THAT(unscale<double>(polygon.length()), Catch::Matchers::WithinAbs(80.0, 0.00001));
for (const Point &p : polygon.points) {
CAPTURE(p.x(), p.y());
CHECK(p.x() >= scale_(0));
CHECK(p.x() <= scale_(20));
CHECK(p.y() >= scale_(0));
CHECK(p.y() <= scale_(20));
const bool on_boundary = p.x() == 0 || p.x() == scale_(20) || p.y() == 0 || p.y() == scale_(20);
CHECK(on_boundary);
}
}
void require_vertex(const Polygon &polygon, const Point &point)
{
// Integer coordinates make the micron transition helpers exact on these axis-aligned edges.
CAPTURE(point.x(), point.y());
REQUIRE(std::find(polygon.points.begin(), polygon.points.end(), point) != polygon.points.end());
}
} // namespace
TEST_CASE("Strong seam modes select the requested location on a clipped side", "[PreciseSeam]")
{
const auto mode = GENERATE(ModelVolumeType::PRECISE_SEAM_LEFT, ModelVolumeType::PRECISE_SEAM_CENTER,
ModelVolumeType::PRECISE_SEAM_RIGHT);
SeamFixture fixture;
// Clipper may collapse all three original collinear edges into one.
Polygon perimeter(Points{mm(0, 0), mm(2, 0), mm(4, 0), mm(8, 0), mm(20, 0), mm(20, 20), mm(0, 20)});
const ModelVolume *modifier = fixture.add(mode, {rectangle(1, -2, 13, 2)});
PreciseSeam::PreciseSeamWarnings warnings;
const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings);
REQUIRE(seam.has_value());
const double expected_x = mode == ModelVolumeType::PRECISE_SEAM_LEFT ? 1.0 :
mode == ModelVolumeType::PRECISE_SEAM_RIGHT ? 13.0 : 7.0;
CHECK(*seam == mm(expected_x, 0));
require_vertex(perimeter, mm(expected_x - 0.001, 0));
require_vertex(perimeter, mm(expected_x + 0.001, 0));
check_square_boundary(perimeter);
CHECK_FALSE(warnings.through_body.load());
CHECK_FALSE(warnings.full_containment.load());
CHECK_FALSE(warnings.multiple_intersections.load());
}
TEST_CASE("Center seams preserve the edge order at vertices and across the contour origin", "[PreciseSeam]")
{
const bool wrap = GENERATE(false, true);
SeamFixture fixture;
Polygon perimeter(Points{mm(0, 0), mm(4, 0), mm(20, 0), mm(20, 20), mm(0, 20)});
// Symmetric cuts put the arc midpoint exactly on an existing vertex, including vertex zero.
const Polygon cut = wrap ? rectangle(-2, -2, 4, 4) : rectangle(1, -2, 7, 2);
const ModelVolume *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {cut});
const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache);
REQUIRE(seam.has_value());
CHECK(*seam == (wrap ? mm(0, 0) : mm(4, 0)));
require_vertex(perimeter, wrap ? mm(0, 0.001) : mm(3.999, 0));
require_vertex(perimeter, wrap ? mm(0.001, 0) : mm(4.001, 0));
check_square_boundary(perimeter);
}
TEST_CASE("Center seams land on the closing edge", "[PreciseSeam]")
{
SeamFixture fixture;
Polygon perimeter = rectangle(0, 0, 20, 20);
const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(-2, 3, 2, 9)});
const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache);
REQUIRE(seam.has_value());
CHECK(*seam == mm(0, 6));
require_vertex(perimeter, mm(0, 5.999));
require_vertex(perimeter, mm(0, 6.001));
check_square_boundary(perimeter);
}
TEST_CASE("Coincident weak boundaries do not prevent later boundary refinement", "[PreciseSeam][Regression]")
{
SeamFixture fixture;
Polygon perimeter = rectangle(0, 0, 20, 20);
// Duplicate boundaries used to stall the reverse cursor before reaching the separate segment.
const auto *a = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(2, -2, 6, 2)});
const auto *b = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(2, -2, 6, 2)});
const auto *c = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(10, -2, 14, 2)});
const auto segments = PreciseSeam::collect_weak_modifier_segments({c, b, a}, perimeter, fixture.layer, fixture.cache);
REQUIRE(segments.size() == 3);
for (double x : {1.999, 6.001, 9.999, 14.001})
require_vertex(perimeter, mm(x, 0));
check_square_boundary(perimeter);
}
TEST_CASE("Weak boundaries sharing a vertex refine both sides", "[PreciseSeam]")
{
SeamFixture fixture;
Polygon perimeter = rectangle(0, 0, 20, 20);
const auto *a = fixture.add(ModelVolumeType::PRECISE_SEAM_BLOCKED, {rectangle(2, -2, 6, 2)});
const auto *b = fixture.add(ModelVolumeType::PRECISE_SEAM_NEUTRAL, {rectangle(6, -2, 10, 2)});
const auto segments = PreciseSeam::collect_weak_modifier_segments({a, b}, perimeter, fixture.layer, fixture.cache);
REQUIRE(segments.size() == 2);
require_vertex(perimeter, mm(5.999, 0));
require_vertex(perimeter, mm(6.001, 0));
check_square_boundary(perimeter);
}
TEST_CASE("Unsupported modifier sections are skipped with the appropriate warning", "[PreciseSeam]")
{
const int scenario = GENERATE(0, 1, 2, 3);
SeamFixture fixture;
Polygon perimeter = rectangle(0, 0, 20, 20);
const Points original = perimeter.points;
Polygons slices;
if (scenario == 0) slices = {rectangle(30, 30, 40, 40)}; // Disjoint bounds.
if (scenario == 1) slices = {rectangle(2, 2, 4, 4)}; // Wholly inside; no common boundary.
if (scenario == 2) slices = {rectangle(-2, -2, 22, 22)}; // Contains the entire perimeter.
if (scenario == 3) {
Polygon hole = rectangle(2, 2, 4, 4);
hole.reverse();
slices = {rectangle(-2, -2, 22, 22), hole};
}
const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, std::move(slices));
PreciseSeam::PreciseSeamWarnings warnings;
CHECK_FALSE(PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings).has_value());
CHECK(perimeter.points == original);
CHECK(warnings.full_containment.load() == (scenario == 2));
CHECK(warnings.multiply_connected.load() == (scenario == 3));
CHECK_FALSE(warnings.multiple_intersections.load());
CHECK_FALSE(warnings.through_body.load());
}
TEST_CASE("Strong modifiers warn when another slice polygon also intersects the perimeter", "[PreciseSeam]")
{
SeamFixture fixture;
Polygon perimeter = rectangle(0, 0, 20, 20);
// One helper has two disconnected sections; only its first section supplies the seam.
const auto *modifier = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER,
{rectangle(2, -2, 6, 2), rectangle(12, -2, 16, 2)});
PreciseSeam::PreciseSeamWarnings warnings;
const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings);
REQUIRE(seam.has_value());
CHECK(*seam == mm(4, 0));
CHECK(warnings.multiple_intersections.load());
CHECK_FALSE(warnings.through_body.load());
CHECK_FALSE(warnings.full_containment.load());
CHECK_FALSE(warnings.multiply_connected.load());
check_square_boundary(perimeter);
}
TEST_CASE("Modifiers crossing the entire body raise a through body warning", "[PreciseSeam]")
{
const bool strong = GENERATE(false, true);
CAPTURE(strong);
SeamFixture fixture;
Polygon perimeter = rectangle(0, 0, 20, 20);
// The strip exits on opposite sides, leaving two exterior pieces. None of the clipped
// vertices matches a square corner, so this also exercises the general segment-extraction path.
const auto type = strong ? ModelVolumeType::PRECISE_SEAM_CENTER : ModelVolumeType::PRECISE_SEAM_BLOCKED;
const auto *modifier = fixture.add(type, {rectangle(8, -2, 12, 22)});
PreciseSeam::PreciseSeamWarnings warnings;
if (strong) {
const auto seam = PreciseSeam::insert_strong_seam_point({modifier}, perimeter, fixture.layer, fixture.cache, &warnings);
REQUIRE(seam.has_value());
CHECK(seam->x() == mm(10, 0).x());
// Either boundary segment may be encountered first by the clipping traversal.
const bool on_crossed_side = seam->y() == 0 || seam->y() == mm(0, 20).y();
CHECK(on_crossed_side);
} else {
const auto segments = PreciseSeam::collect_weak_modifier_segments({modifier}, perimeter, fixture.layer, fixture.cache, &warnings);
REQUIRE_FALSE(segments.empty());
}
CHECK(warnings.through_body.load());
CHECK_FALSE(warnings.multiple_intersections.load()); // The clipped strip is one polygon.
CHECK_FALSE(warnings.full_containment.load());
CHECK_FALSE(warnings.multiply_connected.load());
check_square_boundary(perimeter);
}
TEST_CASE("Modifier hierarchy keeps strong order and applies the highest weak priority last", "[PreciseSeam]")
{
const auto high_type = GENERATE(ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL,
ModelVolumeType::PRECISE_SEAM_ENFORCED);
const auto expected_type = high_type == ModelVolumeType::PRECISE_SEAM_BLOCKED ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Blocked :
high_type == ModelVolumeType::PRECISE_SEAM_NEUTRAL ? SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral :
SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced;
SeamFixture fixture;
const auto *strong_a = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(1, -2, 3, 2)});
const auto *strong_b = fixture.add(ModelVolumeType::PRECISE_SEAM_CENTER, {rectangle(11, -2, 13, 2)});
const auto *high = fixture.add(high_type, {rectangle(2, -2, 6, 2)});
const auto low_type = high_type == ModelVolumeType::PRECISE_SEAM_ENFORCED ? ModelVolumeType::PRECISE_SEAM_BLOCKED :
ModelVolumeType::PRECISE_SEAM_ENFORCED;
const auto *low = fixture.add(low_type, {rectangle(2, -2, 6, 2)});
std::vector<const ModelVolume*> strong, weak;
bool has_strong = false;
PreciseSeam::init_precise_seam_data(strong, weak, has_strong, fixture.modifiers.objects.front());
REQUIRE(has_strong);
CHECK(strong == std::vector<const ModelVolume*>{strong_a, strong_b});
CHECK(weak == std::vector<const ModelVolume*>{low, high});
Polygon perimeter = rectangle(0, 0, 20, 20);
const auto seam = PreciseSeam::insert_strong_seam_point(strong, perimeter, fixture.layer, fixture.cache);
REQUIRE(seam.has_value());
CHECK(*seam == mm(2, 0));
perimeter = rectangle(0, 0, 20, 20);
const auto segments = PreciseSeam::collect_weak_modifier_segments(weak, perimeter, fixture.layer, fixture.cache);
REQUIRE(segments.size() == 2);
PrintObjectSeamData::LayerSeams result;
result.perimeters.emplace_back();
auto &loop = result.perimeters.back();
// Include a preceding candidate to exercise nonzero global layer indices.
result.points.emplace_back(Vec3f(-1, -1, 0), loop, 0, SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral);
loop.start_index = 1;
for (const Point &p : perimeter.points) {
// Match production's double-to-float conversion: weak boundary lookup uses exact equality.
const Vec2f position = unscale(p).cast<float>();
result.points.emplace_back(Vec3f(position.x(), position.y(), 0), loop, 0,
SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral);
}
loop.end_index = result.points.size();
bool enforced = false;
PreciseSeam::apply_weak_modifiers_to_perimeter(segments, result, loop, enforced);
size_t patch_count = 0;
for (size_t i = loop.start_index; i < loop.end_index; ++i) {
const auto &candidate = result.points[i];
// Axis-aligned input and interpolation keep y exactly zero; this classifies, rather than measures, the patch.
const bool in_patch = candidate.position.y() == 0 && candidate.position.x() >= 2 && candidate.position.x() <= 6;
CHECK(candidate.type == (in_patch ? expected_type :
SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral));
if (in_patch) ++patch_count;
}
CHECK(patch_count >= 2);
if (high_type == ModelVolumeType::PRECISE_SEAM_ENFORCED) {
// Four millimetres of enforcement must be subdivided, not just marked at its endpoints.
CHECK(enforced);
CHECK(patch_count >= size_t(4.0f / SeamPlacer::enforcer_oversampling_distance));
}
CHECK(result.points.front().type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Neutral);
}
+281
View File
@@ -0,0 +1,281 @@
#include <catch2/catch_all.hpp>
#include "test_helpers.hpp"
#include "libslic3r/GCode/SeamPlacer.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include <algorithm>
#include <cmath>
using namespace Slic3r;
namespace {
struct PipelineFixture {
Model model;
Print print;
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
explicit PipelineFixture(bool trapezoid = false)
{
auto mesh = its_make_cube(20, 20, 0.4);
if (trapezoid) {
// Opposite painted sides have deliberately different lengths: 20 mm and 6 mm.
for (auto &vertex : mesh.vertices)
if (vertex.y() == 20.0f) vertex.x() = 7.0f + 0.3f * vertex.x();
}
config.set_deserialize_strict("seam_position", "back"); // Rear needs no visibility ray tracing.
config.set_deserialize_strict("layer_height", "0.2");
config.set_deserialize_strict("initial_layer_print_height", "0.2");
config.set_deserialize_strict("outer_wall_line_width", "0.4");
config.set_deserialize_strict("initial_layer_line_width", "0.4");
config.set_deserialize_strict("wall_loops", "1");
config.set_deserialize_strict("raft_layers", "0");
config.set_deserialize_strict("gcode_comments", "1"); // Match init_print so later apply calls change only the model.
Test::init_print({TriangleMesh(std::move(mesh))}, print, model, config);
}
void paint(bool all_faces)
{
auto &volume = *model.objects.front()->volumes.front();
const auto &mesh = volume.mesh();
const auto bounds = mesh.bounding_box();
TriangleSelector selector(mesh);
size_t painted = 0;
for (size_t i = 0; i < mesh.its.indices.size(); ++i) {
const auto &face = mesh.its.indices[i];
bool lower = true, upper = true;
for (int j = 0; j < 3; ++j) {
const auto &v = mesh.its.vertices[face[j]];
lower = lower && std::abs(double(v.y()) - bounds.min.y()) < 1e-6;
upper = upper && std::abs(double(v.y()) - bounds.max.y()) < 1e-6;
}
if (all_faces || lower || upper) {
selector.set_facet(int(i), EnforcerBlockerType::ENFORCER);
++painted;
}
}
REQUIRE(painted > 0);
volume.seam_facets.set(selector);
print.apply(model, config);
}
PrintObject &prepare()
{
REQUIRE(print.objects().size() == 1);
auto &object = *print.get_object(0);
object.slice(); // Real layers/regions are sufficient: each test supplies its own perimeter loops.
REQUIRE_FALSE(object.layers().empty());
REQUIRE_FALSE(object.layers().front()->regions().empty());
return object;
}
Points points_in_layer(const PrintObject &object, const std::vector<Vec2d> &xy) const
{
const auto &volume = *object.model_object()->volumes.front();
const auto minimum = volume.mesh().bounding_box().min;
const Transform3d transform = object.trafo_centered() * volume.get_matrix();
Points points;
for (const auto &point : xy) {
// add_volume centers the mesh; restore its local offset before applying the slicing transform.
const Vec3d local = minimum + Vec3d(point.x(), point.y(), 0.2);
const Vec3d placed = transform * local;
points.emplace_back(scale_(placed.x()), scale_(placed.y()));
}
return points;
}
};
void append_loop(LayerRegion &region, Points points, bool separate_paths = false)
{
// Inject deterministic external loops while keeping the real layer, region and paint-query machinery.
REQUIRE(points.size() >= 3);
points.push_back(points.front());
ExtrusionPaths paths;
if (separate_paths) {
for (size_t i = 1; i < points.size(); ++i) {
ExtrusionPath path(erExternalPerimeter, 0.08, 0.4f, 0.2f);
path.polyline = Polyline3(Polyline(Points{points[i - 1], points[i]}));
paths.push_back(std::move(path));
}
} else {
ExtrusionPath path(erExternalPerimeter, 0.08, 0.4f, 0.2f);
path.polyline = Polyline3(Polyline(std::move(points)));
paths.push_back(std::move(path));
}
region.perimeters.append(ExtrusionLoop(std::move(paths)));
}
LayerRegion &clear_first_layer(PrintObject &object)
{
Layer &layer = *object.layers().front();
for (LayerRegion *region : layer.regions()) region->perimeters.clear();
return *layer.get_region(0);
}
} // namespace
TEST_CASE("Painted seams prefer the longer candidate patch regardless of contour origin", "[SeamPlacer][Regression]")
{
const bool clockwise = GENERATE(false, true);
const bool wrapped = GENERATE(false, true);
CAPTURE(clockwise, wrapped);
PipelineFixture fixture(true);
fixture.paint(false);
PrintObject &object = fixture.prepare();
REQUIRE(object.model_object()->volumes.size() == 1);
CHECK_FALSE(object.model_object()->volumes.front()->is_precise_seam());
auto &region = clear_first_layer(object);
// A neutral loop ensures that patch indices are offsets in the layer, not zero-based local indices.
append_loop(region, fixture.points_in_layer(object, {{9, 8}, {11, 8}, {10, 10}}));
Points outline = fixture.points_in_layer(object, {{10, 20}, {7, 20}, {3.5, 10}, {0, 0}, {10, 0}, {20, 0}, {13, 20}});
if (clockwise) std::reverse(outline.begin() + 1, outline.end()); // Keep the same starting vertex.
if (!wrapped) {
const Point neutral = fixture.points_in_layer(object, {{3.5, 10}}).front();
const auto start = std::find(outline.begin(), outline.end(), neutral);
REQUIRE(start != outline.end());
std::rotate(outline.begin(), start, outline.end());
}
append_loop(region, std::move(outline));
SeamPlacer placer;
placer.init(fixture.print, [] {});
const auto &data = placer.m_seam_per_object.at(&object).layers.front();
REQUIRE(data.perimeters.size() == 2);
const auto &perimeter = data.perimeters[1];
REQUIRE(perimeter.start_index > 0);
REQUIRE(perimeter.end_index > perimeter.start_index);
using Type = SeamPlacerImpl::EnforcedBlockedSeamPoint;
CHECK((data.points[perimeter.start_index].type == Type::Enforced) == wrapped);
if (wrapped) CHECK(data.points[perimeter.end_index - 1].type == Type::Enforced);
const auto extremes = fixture.points_in_layer(object, {{10, 0}, {10, 20}});
const double bottom_y = unscale<double>(extremes[0].y()), top_y = unscale<double>(extremes[1].y());
size_t bottom_count = 0, top_count = 0, centers = 0;
for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i) {
const auto &candidate = data.points[i];
if (candidate.type == Type::Enforced) {
// Include the small paint-radius fringe at the ends of each face.
const bool bottom = std::abs(candidate.position.y() - bottom_y) < 0.5;
const bool top = std::abs(candidate.position.y() - top_y) < 0.5;
const bool on_painted_face = bottom || top;
CAPTURE(candidate.position.x(), candidate.position.y());
CHECK(on_painted_face);
bottom_count += bottom;
top_count += top;
}
if (candidate.central_enforcer) {
++centers;
CHECK(candidate.type == Type::Enforced);
CHECK_THAT(double(candidate.position.y()), Catch::Matchers::WithinAbs(bottom_y, 0.5));
}
}
REQUIRE(top_count > 0);
REQUIRE(bottom_count > top_count);
CHECK(centers == 1); // The old wrapped-length formula instead selected the short top patch.
}
TEST_CASE("Entirely painted contours keep valid enforced seam candidates", "[SeamPlacer]")
{
PipelineFixture fixture;
fixture.paint(true);
PrintObject &object = fixture.prepare();
auto &region = 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);
const auto &perimeter = data.perimeters.front();
CHECK(perimeter.seam_index >= perimeter.start_index);
CHECK(perimeter.seam_index < perimeter.end_index);
for (const auto &candidate : data.points) {
CHECK(candidate.type == SeamPlacerImpl::EnforcedBlockedSeamPoint::Enforced);
CHECK_FALSE(candidate.central_enforcer); // There is no bounded patch to mark as central.
}
}
TEST_CASE("Precise Seam removes path junction duplicates but preserves separate visits", "[SeamPlacer][PreciseSeam]")
{
const bool enable_ps = GENERATE(false, true);
const bool self_touch = GENERATE(false, true);
PipelineFixture fixture;
if (enable_ps) {
auto *helper = fixture.model.objects.front()->add_volume(make_cube(1, 1, 1));
helper->set_type(ModelVolumeType::PRECISE_SEAM_NEUTRAL);
helper->set_offset(Vec3d(100, 100, 0)); // Enable normalization without intersecting the synthetic loop.
fixture.print.apply(fixture.model, fixture.config);
}
PrintObject &object = fixture.prepare();
auto &region = clear_first_layer(object);
const std::vector<Vec2d> vertices = self_touch ? std::vector<Vec2d>{{2, 2}, {10, 2}, {18, 10}, {10, 2}, {2, 18}} :
std::vector<Vec2d>{{2, 2}, {18, 2}, {18, 18}, {2, 18}};
const Points outline = fixture.points_in_layer(object, vertices);
append_loop(region, outline, true);
SeamPlacer placer;
placer.init(fixture.print, [] {});
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.
REQUIRE(data.points.size() == (enable_ps ? outline.size() : 2 * outline.size()));
for (size_t i = 0; i < outline.size(); ++i) {
const Vec2f target = unscale(outline[i]).cast<float>();
const size_t input_count = std::count(outline.begin(), outline.end(), outline[i]);
// Both paths convert the same integer coordinates to float; exact identity detects duplicate copies.
const size_t actual_count = std::count_if(data.points.begin(), data.points.end(), [&](const auto &candidate) {
return candidate.position.template head<2>() == target;
});
CHECK(actual_count == (enable_ps ? input_count : 2 * input_count));
}
if (enable_ps) {
for (size_t i = 0; i < data.points.size(); ++i) {
CHECK(std::isfinite(data.points[i].local_ccw_angle));
CHECK(data.points[i].position != data.points[(i + 1) % data.points.size()].position);
}
}
}
TEST_CASE("Print apply synchronizes support and seam helpers through type changes and restored models", "[SeamPlacer][PreciseSeam][Print]")
{
const int changed = GENERATE(0, 1, 2); // Support only, seam only, or both including cross-family switches.
PipelineFixture fixture;
auto *model_object = fixture.model.objects.front();
auto *support = model_object->add_volume(make_cube(1, 1, 1));
support->set_type(ModelVolumeType::SUPPORT_BLOCKER);
auto *seam = model_object->add_volume(make_cube(1, 1, 1));
seam->set_type(ModelVolumeType::PRECISE_SEAM_CENTER);
fixture.print.apply(fixture.model, fixture.config);
REQUIRE(fixture.print.objects().size() == 1);
const PrintObject *original_print_object = fixture.print.objects().front();
const ModelVolume *original_part = original_print_object->model_object()->volumes.front();
const Model before(fixture.model); // A restored model snapshot preserves IDs, as the apply path requires.
if (changed == 0 || changed == 2) {
support->set_type(changed == 2 ? ModelVolumeType::PRECISE_SEAM_LEFT : ModelVolumeType::SUPPORT_ENFORCER);
support->set_offset(Vec3d(3, 4, 0));
}
if (changed == 1 || changed == 2) {
seam->set_type(changed == 2 ? ModelVolumeType::SUPPORT_BLOCKER : ModelVolumeType::PRECISE_SEAM_RIGHT);
seam->set_offset(Vec3d(-3, 2, 0));
}
if (changed == 2) std::swap(model_object->volumes[1], model_object->volumes[2]);
const auto check_applied = [&](const Model &expected) {
REQUIRE(fixture.print.objects().size() == 1);
// Helper-only changes should preserve the print object and its unaffected printable volume.
CHECK(fixture.print.objects().front() == original_print_object);
const auto &actual = fixture.print.objects().front()->model_object()->volumes;
const auto &wanted = expected.objects.front()->volumes;
REQUIRE(actual.size() == wanted.size());
CHECK(actual.front() == original_part);
for (size_t i = 0; i < wanted.size(); ++i) {
CAPTURE(changed, i);
CHECK(actual[i]->id() == wanted[i]->id());
CHECK(actual[i]->type() == wanted[i]->type());
CHECK(actual[i]->get_matrix().isApprox(wanted[i]->get_matrix(), 1e-9));
}
};
fixture.print.apply(fixture.model, fixture.config);
check_applied(fixture.model);
fixture.print.apply(before, fixture.config);
check_applied(before);
fixture.print.apply(fixture.model, fixture.config);
check_applied(fixture.model);
}
+2
View File
@@ -3,6 +3,8 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp ${_TEST_NAME}_tests.cpp
test_3mf.cpp test_3mf.cpp
# Round-trip seam metadata and active/dormant volume settings in both formats.
test_precise_seam_3mf.cpp
test_aabbindirect.cpp test_aabbindirect.cpp
test_appconfig.cpp test_appconfig.cpp
test_arachne_walls.cpp test_arachne_walls.cpp
+372
View File
@@ -0,0 +1,372 @@
#include <catch2/catch_all.hpp>
#include "test_utils.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Semver.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Zipper.hpp"
#include <algorithm>
#include <array>
#include <string>
#include <vector>
using namespace Slic3r;
namespace {
enum class Backend { Bbs, Prusa };
constexpr std::array<ModelVolumeType, 6> seam_types = {
ModelVolumeType::PRECISE_SEAM_CENTER, ModelVolumeType::PRECISE_SEAM_LEFT,
ModelVolumeType::PRECISE_SEAM_RIGHT, ModelVolumeType::PRECISE_SEAM_ENFORCED,
ModelVolumeType::PRECISE_SEAM_BLOCKED, ModelVolumeType::PRECISE_SEAM_NEUTRAL
};
// Literal entities and backslashes distinguish XML escaping from config serialization.
const std::string notes = "quoted \"value\" & <tag>\tcolumn\nnext line &amp; path\\file";
struct Scene {
// Keep the backup directory alive longer than its model; no project files are touched.
ScopedTemporaryDir backup{"orca_seam_3mf"};
Model model;
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
bool share_mesh = false;
Scene()
{
model.set_backup_path(backup.string());
// Import normalizes extruder indices against this list; explicitly provide filament 2.
config.set_key_value("filament_settings_id", new ConfigOptionStrings(std::vector<std::string>{"A", "B"}));
}
void populate(bool all_modes, bool shared_mesh = false)
{
share_mesh = shared_mesh;
auto *object = model.add_object();
object->name = "seam round trip";
auto *part = object->add_volume(make_cube(20, 20, 2));
part->name = "printable";
ModelVolume *first_helper = nullptr;
for (size_t i = 0; i < (all_modes ? seam_types.size() : size_t(1)); ++i) {
auto *volume = shared_mesh && first_helper ? object->add_volume_with_shared_mesh(*first_helper) :
object->add_volume(make_cube(2, 3, 4));
if (!first_helper) first_helper = volume;
volume->name = "helper_" + std::to_string(i);
volume->set_type(seam_types[i]);
Geometry::Transformation transform;
transform.set_offset(Vec3d(3.0 * double(i), -2.0, 1.0));
transform.set_rotation(Vec3d(0.0, 0.0, 0.1 * double(i + 1)));
transform.set_scaling_factor(Vec3d(1.0, 1.2, 0.8));
volume->set_transformation(transform);
// Simulate settings retained by conversion from a part/modifier into a seam helper.
volume->config.set_key_value("extruder", new ConfigOptionInt(2));
volume->config.set_key_value("sparse_infill_density", new ConfigOptionPercent(100.0 - 5.0 * double(i)));
volume->config.set_key_value("notes", new ConfigOptionString(notes + std::to_string(i)));
}
object->add_instance();
}
};
void save(const std::string &path, Backend backend, Scene &scene)
{
if (backend == Backend::Prusa) {
REQUIRE(store_3mf(path.c_str(), &scene.model, &scene.config, false));
} else {
StoreParams params;
params.path = path.c_str();
params.model = &scene.model;
params.config = &scene.config;
params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
if (scene.share_mesh) params.strategy = params.strategy | SaveStrategy::ShareMesh;
REQUIRE(store_bbs_3mf(params));
}
}
void load(const std::string &path, Backend backend, Scene &scene)
{
ConfigSubstitutionContext substitutions{ForwardCompatibilitySubstitutionRule::Enable};
if (backend == Backend::Prusa) {
REQUIRE(load_3mf(path.c_str(), scene.config, substitutions, &scene.model, false));
} else {
struct ImportedResources {
PlateDataPtrs plates;
std::vector<Preset*> presets;
// Catch failures must not leak importer-owned auxiliary objects.
~ImportedResources() { release_PlateData_list(plates); for (auto *preset : presets) delete preset; }
} resources;
bool bbs = false, orca = false;
Semver version;
REQUIRE(load_bbs_3mf(path.c_str(), &scene.config, &substitutions, &scene.model, &resources.plates,
&resources.presets, &bbs, &orca, &version, nullptr,
LoadStrategy::LoadModel | LoadStrategy::LoadConfig));
}
}
using Archive = std::vector<std::pair<std::string, std::string>>;
Archive read_archive(const std::string &path)
{
struct Reader {
mz_zip_archive zip{};
~Reader() { if (zip.m_pState) close_zip_reader(&zip); }
} reader;
REQUIRE(open_zip_reader(&reader.zip, path));
Archive entries;
for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&reader.zip); ++i) {
mz_zip_archive_file_stat stat;
REQUIRE(mz_zip_reader_file_stat(&reader.zip, i, &stat));
if (stat.m_is_directory) continue;
std::string name(stat.m_filename);
std::replace(name.begin(), name.end(), '\\', '/');
std::string data(size_t(stat.m_uncomp_size), '\0');
if (!data.empty()) REQUIRE(mz_zip_reader_extract_to_mem(&reader.zip, i, data.data(), data.size(), 0));
entries.emplace_back(std::move(name), std::move(data));
}
return entries;
}
std::string &model_xml(Archive &archive, Backend backend)
{
const std::string name = backend == Backend::Bbs ? "Metadata/model_settings.config" : "Metadata/Slic3r_PE_model.config";
const auto found = std::find_if(archive.begin(), archive.end(), [&](const auto &entry) { return entry.first == name; });
REQUIRE(found != archive.end());
return found->second;
}
void write_archive(const std::string &path, const Archive &archive)
{
// Repack a separate temporary archive, preserving every entry except explicitly edited metadata.
Zipper zipper(path);
for (const auto &entry : archive) zipper.add_entry(entry.first, entry.second.data(), entry.second.size());
zipper.finalize();
}
void replace_once(std::string &text, const std::string &from, const std::string &to)
{
const auto pos = text.find(from);
REQUIRE(pos != std::string::npos);
REQUIRE(text.find(from, pos + from.size()) == std::string::npos);
text.replace(pos, from.size(), to);
}
std::string remove_mode_metadata(std::string &xml)
{
// Mutations must locate exactly one real metadata element; silent no-op rewrites would give false confidence.
const auto key = xml.find("key=\"precise_seam_type\"");
REQUIRE(key != std::string::npos);
REQUIRE(xml.find("key=\"precise_seam_type\"", key + 1) == std::string::npos);
const auto begin = xml.rfind("<metadata ", key);
const auto end = xml.find("/>", key);
REQUIRE(begin != std::string::npos);
REQUIRE(end != std::string::npos);
const std::string result = xml.substr(begin, end + 2 - begin);
xml.erase(begin, result.size());
return result;
}
void check_config(const ModelVolume &volume, size_t index = 0)
{
REQUIRE(volume.config.has("extruder"));
CHECK(volume.config.opt_int("extruder") == 2);
REQUIRE(volume.config.has("sparse_infill_density"));
CHECK_THAT(volume.config.opt_float("sparse_infill_density"), Catch::Matchers::WithinAbs(100.0 - 5.0 * double(index), 1e-9));
REQUIRE(volume.config.has("notes"));
CHECK(volume.config.get().opt_string("notes") == notes + std::to_string(index));
}
void check_modifier_config(const ModelVolume &volume, Backend backend, size_t index = 0)
{
if (backend == Backend::Bbs) {
check_config(volume, index);
} else {
// The Prusa importer whitelists extruder, but drops ordinary notes and infill settings.
REQUIRE(volume.config.has("extruder"));
CHECK(volume.config.opt_int("extruder") == 2);
CHECK_FALSE(volume.config.has("sparse_infill_density"));
CHECK_FALSE(volume.config.has("notes"));
}
}
void check_geometry(const ModelVolume &before, const ModelVolume &after)
{
// Importers may recenter a mesh and compensate in its transform; compare transformed vertices.
REQUIRE(after.mesh().its.vertices.size() == before.mesh().its.vertices.size());
CHECK(after.mesh().its.indices.size() == before.mesh().its.indices.size());
std::vector<Vec3d> expected;
for (const auto &v : before.mesh().its.vertices) expected.push_back(before.get_matrix() * v.cast<double>());
for (const auto &v : after.mesh().its.vertices) {
const Vec3d actual = after.get_matrix() * v.cast<double>();
const auto match = std::find_if(expected.begin(), expected.end(), [&](const Vec3d &p) { return (p - actual).norm() < 1e-4; });
REQUIRE(match != expected.end());
expected.erase(match); // Match multiplicities, not merely membership in the vertex set.
}
CHECK(expected.empty());
}
} // namespace
TEST_CASE("All Precise Seam types and dormant settings survive a 3MF round trip", "[PreciseSeam3mf]")
{
const auto backend = GENERATE(Backend::Bbs, Backend::Prusa);
const bool shared = GENERATE(false, true);
CAPTURE(int(backend), shared);
Scene source;
source.populate(true, shared);
ScopedTemporaryFile file(".3mf");
save(file.string(), backend, source);
Archive archive = read_archive(file.string());
const auto &xml = model_xml(archive, backend);
const std::string open = backend == Backend::Bbs ? "<part " : "<volume ";
const std::string close = backend == Backend::Bbs ? "</part>" : "</volume>";
size_t pos = 0, helpers = 0;
while ((pos = xml.find(open, pos)) != std::string::npos) {
const auto end = xml.find(close, pos);
REQUIRE(end != std::string::npos);
const auto block = xml.substr(pos, end - pos);
if (block.find("key=\"precise_seam_type\"") != std::string::npos) {
++helpers;
CHECK(block.find(backend == Backend::Bbs ? "subtype=\"modifier_part\"" : "value=\"ParameterModifier\"") != std::string::npos);
for (const std::string key : {"extruder", "sparse_infill_density", "notes"}) {
CAPTURE(key);
CHECK(block.find("key=\"" + key + "\"") == std::string::npos);
CHECK(block.find("key=\"precise_seam_config:" + key + "\"") != std::string::npos);
}
}
pos = end + close.size();
}
REQUIRE(helpers == seam_types.size());
Scene destination;
load(file.string(), backend, destination);
REQUIRE(destination.model.objects.size() == 1);
const auto &volumes = destination.model.objects.front()->volumes;
REQUIRE(volumes.size() == 1 + seam_types.size());
CHECK(volumes.front()->is_model_part());
for (size_t i = 0; i < seam_types.size(); ++i) {
CAPTURE(i);
CHECK(volumes[i + 1]->type() == seam_types[i]);
CHECK(volumes[i + 1]->name == "helper_" + std::to_string(i));
check_geometry(*source.model.objects.front()->volumes[i + 1], *volumes[i + 1]);
check_config(*volumes[i + 1], i);
if (shared && backend == Backend::Bbs)
CHECK(volumes[i + 1]->mesh_ptr().get() == volumes[1]->mesh_ptr().get());
volumes[i + 1]->set_type(ModelVolumeType::PARAMETER_MODIFIER);
}
// Reverse conversion must survive another file round trip, not just retain config in memory.
destination.share_mesh = shared;
ScopedTemporaryFile converted_file(".3mf");
save(converted_file.string(), backend, destination);
Archive converted_archive = read_archive(converted_file.string());
const auto &converted_xml = model_xml(converted_archive, backend);
CHECK(converted_xml.find("key=\"precise_seam_type\"") == std::string::npos);
CHECK(converted_xml.find("key=\"precise_seam_config:") == std::string::npos);
Scene converted;
load(converted_file.string(), backend, converted);
REQUIRE(converted.model.objects.size() == 1);
const auto &converted_volumes = converted.model.objects.front()->volumes;
REQUIRE(converted_volumes.size() == volumes.size());
for (size_t i = 0; i < seam_types.size(); ++i) {
CAPTURE(i);
CHECK(converted_volumes[i + 1]->type() == ModelVolumeType::PARAMETER_MODIFIER);
CHECK(converted_volumes[i + 1]->name == "helper_" + std::to_string(i));
check_geometry(*volumes[i + 1], *converted_volumes[i + 1]);
check_modifier_config(*converted_volumes[i + 1], backend, i);
}
}
TEST_CASE("Ordinary modifier settings are escaped once in 3MF attributes", "[PreciseSeam3mf][Regression]")
{
const auto backend = GENERATE(Backend::Bbs, Backend::Prusa);
CAPTURE(int(backend));
Scene source;
source.populate(false);
source.model.objects.front()->volumes[1]->set_type(ModelVolumeType::PARAMETER_MODIFIER);
ScopedTemporaryFile file(".3mf");
save(file.string(), backend, source);
Archive archive = read_archive(file.string());
const auto &xml = model_xml(archive, backend);
// Check the serialized value independently of the writer's escaping helper. Prusa drops
// ordinary notes on import, so successful loading alone would not detect double escaping.
const std::string encoded_notes = R"(quoted \&quot;value\&quot; &amp; &lt;tag>&#x9;column\nnext line &amp;amp; path\\file0)";
CHECK(xml.find("key=\"notes\" value=\"" + encoded_notes + "\"") != std::string::npos);
Scene destination;
load(file.string(), backend, destination);
REQUIRE(destination.model.objects.size() == 1);
REQUIRE(destination.model.objects.front()->volumes.size() == 2);
const ModelVolume &modifier = *destination.model.objects.front()->volumes[1];
CHECK(modifier.type() == ModelVolumeType::PARAMETER_MODIFIER);
check_modifier_config(modifier, backend);
}
TEST_CASE("Seam metadata restores only recognized modes on compatible base types", "[PreciseSeam3mf][Regression]")
{
const auto backend = GENERATE(Backend::Bbs, Backend::Prusa);
// Include both XML orders, fallback cases, and the previous inline type representation.
const int variant = GENERATE(0, 1, 2, 3, 4, 5);
CAPTURE(int(backend), variant);
Scene source;
source.populate(false);
ScopedTemporaryFile original(".3mf"), edited(".3mf");
save(original.string(), backend, source);
Archive archive = read_archive(original.string());
auto &xml = model_xml(archive, backend);
if (variant == 0 || variant == 1) {
const std::string metadata = remove_mode_metadata(xml);
const std::string tag = backend == Backend::Bbs ? "part" : "volume";
// Locate the helper via its dormant config, then move its mode before/after base metadata.
const auto key = xml.find("key=\"precise_seam_config:extruder\"");
REQUIRE(key != std::string::npos);
const auto start = xml.rfind("<" + tag + " ", key);
REQUIRE(start != std::string::npos);
if (backend == Backend::Bbs) {
// Exercise base-type metadata too, not just the subtype attribute preceding all metadata.
const auto end = xml.find("</part>", key);
REQUIRE(end != std::string::npos);
xml.insert(end, "<metadata key=\"part_type\" value=\"modifier_part\"/>");
}
const auto opening_end = xml.find('>', start);
REQUIRE(opening_end != std::string::npos);
const auto insertion = variant == 0 ? opening_end + 1 : xml.find("</" + tag + ">", key);
REQUIRE(insertion != std::string::npos);
xml.insert(insertion, metadata);
} else if (variant == 2) {
replace_once(xml, "value=\"precise_seam_center\"", "value=\"unknown_future_seam\"");
} else if (variant == 3) {
remove_mode_metadata(xml);
} else if (variant == 4) {
// A known seam mode must not reinterpret an ordinary printable part.
if (backend == Backend::Bbs) replace_once(xml, "subtype=\"modifier_part\"", "subtype=\"normal_part\"");
else {
replace_once(xml, "key=\"modifier\" value=\"1\"", "key=\"modifier\" value=\"0\"");
replace_once(xml, "value=\"ParameterModifier\"", "value=\"ModelPart\"");
}
} else {
remove_mode_metadata(xml);
if (backend == Backend::Bbs) replace_once(xml, "subtype=\"modifier_part\"", "subtype=\"precise_seam_center\"");
else replace_once(xml, "value=\"ParameterModifier\"", "value=\"precise_seam_center\"");
// Older files stored settings as ordinary volume keys, without the new dormant namespace.
for (const std::string key : {"extruder", "sparse_infill_density", "notes"})
replace_once(xml, "key=\"precise_seam_config:" + key + "\"", "key=\"" + key + "\"");
}
write_archive(edited.string(), archive);
Scene destination;
load(edited.string(), backend, destination);
REQUIRE(destination.model.objects.size() == 1);
REQUIRE(destination.model.objects.front()->volumes.size() == 2);
const ModelVolume &helper = *destination.model.objects.front()->volumes[1];
if (variant == 0 || variant == 1 || variant == 5) {
CHECK(helper.type() == ModelVolumeType::PRECISE_SEAM_CENTER);
if (variant == 5 && backend == Backend::Prusa) {
// The legacy Prusa whitelist only accepted extruder among these ordinary setting keys.
REQUIRE(helper.config.has("extruder"));
CHECK(helper.config.opt_int("extruder") == 2);
CHECK_FALSE(helper.config.has("sparse_infill_density"));
CHECK_FALSE(helper.config.has("notes"));
} else check_config(helper);
} else {
CHECK(helper.type() == (variant == 4 ? ModelVolumeType::MODEL_PART : ModelVolumeType::PARAMETER_MODIFIER));
CHECK_FALSE(helper.config.has("extruder"));
CHECK_FALSE(helper.config.has("sparse_infill_density"));
CHECK_FALSE(helper.config.has("notes"));
}
}
+2
View File
@@ -12,6 +12,8 @@ add_executable(${_TEST_NAME}_tests
test_network_versions.cpp test_network_versions.cpp
test_action_source.cpp test_action_source.cpp
test_plugin_host_api.cpp test_plugin_host_api.cpp
# Exercise seam enums and predicates through the embedded Python host API.
test_precise_seam_plugin.cpp
test_plugin_capability_config.cpp test_plugin_capability_config.cpp
test_plugin_config.cpp test_plugin_config.cpp
test_plugin_capabilities_in_use.cpp test_plugin_capabilities_in_use.cpp
@@ -0,0 +1,61 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "python_test_support.hpp"
#include <pybind11/embed.h>
#include <array>
namespace py = pybind11;
using namespace Slic3r;
TEST_CASE("Python exposes every seam mode and distinguishes strong weak and ordinary volumes", "[PreciseSeamPlugin][Python]")
{
struct Mode {
const char *name;
ModelVolumeType type;
bool strong;
bool weak;
};
// Explicit expectations protect enum registration and both predicate groups independently.
const std::array<Mode, 11> modes = {{
{"PreciseSeamCenter", ModelVolumeType::PRECISE_SEAM_CENTER, true, false},
{"PreciseSeamLeft", ModelVolumeType::PRECISE_SEAM_LEFT, true, false},
{"PreciseSeamRight", ModelVolumeType::PRECISE_SEAM_RIGHT, true, false},
{"PreciseSeamEnforced", ModelVolumeType::PRECISE_SEAM_ENFORCED, false, true},
{"PreciseSeamBlocked", ModelVolumeType::PRECISE_SEAM_BLOCKED, false, true},
{"PreciseSeamNeutral", ModelVolumeType::PRECISE_SEAM_NEUTRAL, false, true},
{"ModelPart", ModelVolumeType::MODEL_PART, false, false},
{"NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME, false, false},
{"ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER, false, false},
{"SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER, false, false},
{"SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER, false, false}
}};
// Bootstrap holds the GIL on first initialization; acquire explicitly for an already-running interpreter too.
ensure_python_initialized();
py::gil_scoped_acquire gil;
py::object host = import_orca_module().attr("host");
REQUIRE(py::hasattr(host, "ModelVolumeType"));
py::object enumeration = host.attr("ModelVolumeType");
for (const Mode &mode : modes) {
DYNAMIC_SECTION(mode.name) {
REQUIRE(py::hasattr(enumeration, mode.name));
CHECK(enumeration.attr(mode.name).cast<ModelVolumeType>() == mode.type);
Model model;
auto *volume = model.add_object()->add_volume(make_cube(1, 1, 1));
volume->set_type(mode.type);
// Borrow the volume: the Python reference is destroyed before its owning Model.
py::object py_volume = py::cast(volume, py::return_value_policy::reference);
REQUIRE(py::hasattr(py_volume, "is_precise_seam"));
REQUIRE(py::hasattr(py_volume, "is_precise_seam_strong"));
REQUIRE(py::hasattr(py_volume, "is_precise_seam_weak"));
CHECK(py_volume.attr("type")().cast<ModelVolumeType>() == mode.type);
const bool precise = mode.strong || mode.weak;
CHECK(py_volume.attr("is_precise_seam")().cast<bool>() == precise);
CHECK(py_volume.attr("is_precise_seam_strong")().cast<bool>() == mode.strong);
CHECK(py_volume.attr("is_precise_seam_weak")().cast<bool>() == mode.weak);
}
}
}