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>
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 |
@@ -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()) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,11 +3157,18 @@ 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 << "=\"" << ModelVolume::type_to_string(volume->type()) << "\"/>\n";
|
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";
|
||||||
|
|
||||||
// stores volume's local matrix
|
// stores volume's local matrix
|
||||||
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\"";
|
stream << " <" << METADATA_TAG << " " << TYPE_ATTR << "=\"" << VOLUME_TYPE << "\" " << KEY_ATTR << "=\"" << MATRIX_KEY << "\" " << VALUE_ATTR << "=\"";
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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_
|
||||||
@@ -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 ¤t = 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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
if (precise_seam_differ) {
|
||||||
model_volume_list_update_supports(model_object, model_object_new);
|
// 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;
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ public:
|
|||||||
SlicingReplaceInitEmptyLayers,
|
SlicingReplaceInitEmptyLayers,
|
||||||
SlicingNeedSupportOn,
|
SlicingNeedSupportOn,
|
||||||
SlicingEmptyGcodeLayers,
|
SlicingEmptyGcodeLayers,
|
||||||
SlicingGcodeOverlap
|
SlicingGcodeOverlap,
|
||||||
|
SlicingPreciseSeamWarning
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef size_t TimeStamp;
|
typedef size_t TimeStamp;
|
||||||
|
|||||||
@@ -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,27 +1571,26 @@ 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)
|
slices.emplace_back(to_polygons(std::move(src)));
|
||||||
slices.emplace_back(to_polygons(std::move(src)));
|
} else if (!slices2.empty()) {
|
||||||
} else if (!slices2.empty()) {
|
if (merge_layers.empty())
|
||||||
if (merge_layers.empty())
|
merge_layers.assign(zs.size(), false);
|
||||||
merge_layers.assign(zs.size(), false);
|
for (size_t i = 0; i < zs.size(); ++ i) {
|
||||||
for (size_t i = 0; i < zs.size(); ++ i) {
|
if (slices[i].empty())
|
||||||
if (slices[i].empty())
|
slices[i] = to_polygons(std::move(slices2[i]));
|
||||||
slices[i] = to_polygons(std::move(slices2[i]));
|
else if (! slices2[i].empty()) {
|
||||||
else if (! slices2[i].empty()) {
|
append(slices[i], to_polygons(std::move(slices2[i])));
|
||||||
append(slices[i], to_polygons(std::move(slices2[i])));
|
merge_layers[i] = true;
|
||||||
merge_layers[i] = true;
|
merge = true;
|
||||||
merge = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (merge) {
|
if (merge) {
|
||||||
std::vector<Polygons*> to_merge;
|
std::vector<Polygons*> to_merge;
|
||||||
to_merge.reserve(zs.size());
|
to_merge.reserve(zs.size());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -843,11 +860,16 @@ wxMenuItem* MenuFactory::append_menu_item_change_type(wxMenu* menu)
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<TypeInfo> types = {
|
std::vector<TypeInfo> types = {
|
||||||
{ ModelVolumeType::MODEL_PART, _L("Part") },
|
{ ModelVolumeType::MODEL_PART, _L("Part") },
|
||||||
{ 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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()); })
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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 ®ion, 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 ®ion = 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 ®ion = clear_first_layer(object);
|
||||||
|
append_loop(region, fixture.points_in_layer(object, {{0, 0}, {20, 0}, {20, 20}, {0, 20}}));
|
||||||
|
SeamPlacer placer;
|
||||||
|
placer.init(fixture.print, [] {});
|
||||||
|
const auto &data = placer.m_seam_per_object.at(&object).layers.front();
|
||||||
|
REQUIRE(data.perimeters.size() == 1);
|
||||||
|
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 ®ion = 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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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 & 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 \"value\" & <tag>	column\nnext line &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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||