mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 06:01:16 +00:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce9eb05747 | ||
|
|
c9d897b4d0 | ||
|
|
6bff3c3cd6 | ||
|
|
6e1d15f8a2 | ||
|
|
4f63cd4051 | ||
|
|
c7489d431d | ||
|
|
506dd497db | ||
|
|
56da2f43b0 | ||
|
|
00838d59c1 | ||
|
|
8f8bfd6a28 | ||
|
|
90a5a0b964 | ||
|
|
f64beb4379 | ||
|
|
a2b1a2f3e7 | ||
|
|
0585836550 | ||
|
|
5a36120c58 | ||
|
|
69193d5681 | ||
|
|
cdb9bb57ae |
No files matched your search
@@ -19,6 +19,8 @@ on:
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/check_exception_throw_info.ps1'
|
||||
- 'scripts/test_check_exception_throw_info.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
@@ -41,6 +43,8 @@ on:
|
||||
- 'build_linux.sh'
|
||||
- 'build_win.bat'
|
||||
- 'scripts/test_build_win.ps1'
|
||||
- 'scripts/check_exception_throw_info.ps1'
|
||||
- 'scripts/test_check_exception_throw_info.ps1'
|
||||
- 'build_release_macos.sh'
|
||||
- 'scripts/build_preset_cache.*'
|
||||
- 'scripts/flatpak/**'
|
||||
@@ -84,6 +88,10 @@ jobs:
|
||||
shell: powershell
|
||||
run: .\scripts\test_build_win.ps1
|
||||
|
||||
- name: Run the exception throw info check tests
|
||||
shell: powershell
|
||||
run: .\scripts\test_check_exception_throw_info.ps1
|
||||
|
||||
build_linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -65,15 +65,6 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
|
||||
# clang-cl refuses a precompiled header from another cl.exe build and ccache
|
||||
# does not hash that build, so each one gets its own cache. The build number
|
||||
# is read from cl.exe itself; the toolset directory keeps its name across patches.
|
||||
if [ "${{ runner.os }}" = Windows ]; then
|
||||
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
|
||||
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
|
||||
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
|
||||
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
|
||||
fi
|
||||
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -552,6 +543,13 @@ jobs:
|
||||
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
|
||||
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
|
||||
|
||||
- name: Check exception throw info Win
|
||||
if: runner.os == 'Windows' && inputs.compiler == 'clang'
|
||||
working-directory: ${{ github.workspace }}
|
||||
# After the uploads, so a failure still leaves the build and its PDB to inspect, and before any deploy.
|
||||
shell: pwsh
|
||||
run: scripts\check_exception_throw_info.ps1 -BuildDir $env:BUILD_DIR
|
||||
|
||||
- name: Deploy Windows release portable
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
|
||||
uses: ./.github/actions/deploy-nightly
|
||||
|
||||
@@ -321,6 +321,14 @@ if (IS_CROSS_COMPILE)
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
endif ()
|
||||
|
||||
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
|
||||
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
|
||||
if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
message(STATUS "clang-cl: building without the precompiled header (llvm/llvm-project#53486)")
|
||||
set_property(CACHE SLIC3R_PCH PROPERTY HELPSTRING "Use precompiled headers (ignored with clang-cl, llvm/llvm-project#53486)")
|
||||
set(SLIC3R_PCH OFF)
|
||||
endif ()
|
||||
|
||||
# Print out the SLIC3R_* cache options
|
||||
get_cmake_property(_cache_vars CACHE_VARIABLES)
|
||||
list (SORT _cache_vars)
|
||||
|
||||
+2
-2
@@ -473,8 +473,8 @@ fi
|
||||
# cmake 4.x compatibility workaround
|
||||
export CMAKE_POLICY_VERSION_MINIMUM=3.5
|
||||
|
||||
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d '"')
|
||||
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d '"')
|
||||
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d "'\"")
|
||||
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d "'\"")
|
||||
# Check for direct distribution match to Ubuntu/Debian
|
||||
if [ "${DISTRIBUTION}" == "ubuntu" ] || [ "${DISTRIBUTION}" == "linuxmint" ] ; then
|
||||
DISTRIBUTION="debian"
|
||||
|
||||
+7
-4
@@ -53,7 +53,7 @@ call :add_section "Build configuration"
|
||||
call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)"
|
||||
call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)"
|
||||
call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled"
|
||||
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by --cache"
|
||||
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by -l and --cache"
|
||||
|
||||
call :add_section "Toolchain"
|
||||
call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler"
|
||||
@@ -388,6 +388,9 @@ if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" (
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM CMakeLists turns the precompiled header off for clang-cl (llvm/llvm-project#53486).
|
||||
if "%use_clang_cl%" == "ON" set "no_pch=ON"
|
||||
|
||||
set "generator=!vs_gen_%vs_version%!"
|
||||
if "%use_ninja%" == "ON" (
|
||||
set "generator=Ninja Multi-Config"
|
||||
@@ -543,7 +546,7 @@ REM real path rather than one glued onto the repository root.
|
||||
for %%p in ("!build_dir!") do set "build_full=%%~fp"
|
||||
echo Configuration: %build_type%, %arch%
|
||||
if not "%clang_exe%" == "" echo Compiler: %clang_exe%
|
||||
if "%no_pch%" == "ON" echo Precompiled header: off
|
||||
if "%build_slicer%" == "ON" if "%no_pch%" == "ON" echo Precompiled header: off
|
||||
if not "%cache_args%" == "" echo Compiler cache: %cache_exe%
|
||||
|
||||
set "SIG_FLAG="
|
||||
@@ -716,8 +719,8 @@ if "%build_slicer%" == "ON" (
|
||||
set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON"
|
||||
)
|
||||
|
||||
REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because
|
||||
REM CMake takes the last definition on the command line.
|
||||
REM Under cl, a later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins,
|
||||
REM because CMake takes the last definition on the command line.
|
||||
if "%no_pch%" == "ON" (
|
||||
set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF"
|
||||
)
|
||||
|
||||
Vendored
+5
-2
@@ -26,6 +26,7 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
|
||||
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277
|
||||
PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch
|
||||
DEPENDS ${JPEG_PKG}
|
||||
CMAKE_ARGS
|
||||
-DBUILD_SHARED_LIBS=0
|
||||
-DBUILD_PERE_TESTS=OFF
|
||||
@@ -34,7 +35,8 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
-DBUILD_EXAMPLES=OFF
|
||||
-DBUILD_JASPER=OFF
|
||||
-DBUILD_JAVA=OFF
|
||||
-DBUILD_JPEG=ON
|
||||
# Share Orca's libjpeg to avoid conflicting codec symbols in the application.
|
||||
-DBUILD_JPEG=OFF
|
||||
-DBUILD_APPS_LIST=version
|
||||
-DBUILD_opencv_apps=OFF
|
||||
-DBUILD_opencv_java=OFF
|
||||
@@ -67,7 +69,8 @@ orcaslicer_add_cmake_project(OpenCV
|
||||
-DWITH_OPENJPEG=OFF
|
||||
-DWITH_QUIRC=OFF
|
||||
-DWITH_VTK=OFF
|
||||
-DWITH_JPEG=OFF
|
||||
# Textured GLB/OBJ imports decode embedded and external JPEGs with cv::imdecode.
|
||||
-DWITH_JPEG=ON
|
||||
-DWITH_WEBP=OFF
|
||||
-DWITH_TIFF=OFF
|
||||
-DBUILD_TIFF=OFF
|
||||
|
||||
Vendored
+9
-1
@@ -21,6 +21,14 @@ else ()
|
||||
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
|
||||
endif ()
|
||||
|
||||
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
|
||||
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
|
||||
set(_wx_precomp OFF)
|
||||
else ()
|
||||
set(_wx_precomp ON)
|
||||
endif ()
|
||||
|
||||
set(_wx_patch_command "")
|
||||
if (APPLE)
|
||||
set(_wx_patch_command
|
||||
@@ -39,7 +47,7 @@ orcaslicer_add_cmake_project(
|
||||
PATCH_COMMAND ${_wx_patch_command}
|
||||
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
|
||||
CMAKE_ARGS
|
||||
-DwxBUILD_PRECOMP=ON
|
||||
-DwxBUILD_PRECOMP=${_wx_precomp}
|
||||
${_wx_toolkit}
|
||||
"-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}"
|
||||
-DwxBUILD_DEBUG_LEVEL=0
|
||||
|
||||
@@ -350,8 +350,11 @@ identity default and translates nothing. `NetworkAgent` forwards both to the liv
|
||||
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
|
||||
while no agent is live. Whole documents are Bambu's business alone:
|
||||
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
|
||||
`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
|
||||
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
|
||||
`tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not
|
||||
parse, or carries none of those keys, comes back unchanged. The last two are the H2C nozzle rack's:
|
||||
`cate` in the auto nozzle mapping request, `fila_id` in the rack status. A new Bambu message that
|
||||
carries a filament id under another key needs that key added to `filament_id_keys`, or the id
|
||||
crosses the boundary untranslated. The map is loaded once, lazily; a missing or malformed
|
||||
file degrades to identity with a log line rather than failing.
|
||||
|
||||
| Boundary | Where it translates |
|
||||
|
||||
@@ -62,23 +62,6 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
|
||||
clip do not leave slivers.
|
||||
- Open polylines are clipped with the non-zero rule and keep their direction.
|
||||
|
||||
### Tiled booleans
|
||||
|
||||
The sweep slows down with the number of edges crossing a scan line, so a layer
|
||||
cut into thousands of pieces makes every whole-layer boolean expensive.
|
||||
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
|
||||
non-overlapping `ExPolygons`, group them into tiles with
|
||||
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
|
||||
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
|
||||
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
|
||||
|
||||
The result covers the same area as the plain call. Without the safety offset
|
||||
the rings are the same. With it, each tile unites only the clip polygons near
|
||||
it, so a clip edge that the whole-layer union splits where it crosses a distant
|
||||
clip polygon stays whole, and a crossing with the subject can round 1 unit
|
||||
differently. The tiles' results are concatenated in tile order, so the order of
|
||||
the output `ExPolygons` differs from the plain call.
|
||||
|
||||
### Offsets
|
||||
|
||||
- Before offsetting, input vertices closer than
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# Travel optimization — High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
Inside an island, the infill paths of each region are chained into one print
|
||||
order by a greedy pass that joins the closest free path ends first. That pass
|
||||
leaves its longest joins for last, and they often cross the whole island. Two
|
||||
things shorten the travel between infill paths:
|
||||
|
||||
- the fill generators join their lines into as few paths as they can, since
|
||||
every separate path needs a travel to it;
|
||||
- the G-code export refines the greedy chain with a local search that shortens
|
||||
the straight-line travel between the paths.
|
||||
|
||||
The refinement covers every chain that `chain_and_reorder_extrusion_entities()`
|
||||
builds from a start point: the infill and ironing of `GCode::extrude_infill()`
|
||||
and the support of `GCode::extrude_support()`, as well as the order of objects
|
||||
and islands under `print_order` Default. It changes only the order and the
|
||||
direction of the paths, not the paths or the walls. Neither part has a setting.
|
||||
|
||||
## The chain
|
||||
|
||||
`ObjectByExtruder::Island::Region::append()` flattens every sortable fill
|
||||
collection into its paths, so the paths of all the fills of a region in one
|
||||
island are chained together. A collection that must keep its order (`no_sort`:
|
||||
monotonic, concentric, a forced fill order, the flow calibration) stays a single
|
||||
entry that cannot be reversed. Paths that must keep their direction cannot be
|
||||
reversed either: Grid's, since it alternates their direction between layers for
|
||||
strength, and those of a fill with a forced order. A loop starts and ends at the
|
||||
same point, so reversing it changes nothing.
|
||||
|
||||
`extrude_infill()` chains each region from the current nozzle position with
|
||||
`chain_and_reorder_extrusion_entities()`, and chains the paths inside a nested
|
||||
sortable collection the same way; `extrude_support()` chains the support paths
|
||||
of a layer so too, unless they keep their order. The overload without a start
|
||||
point, used to chain collections while slicing, keeps the greedy chain. The
|
||||
order the fill generators give their paths does not reach the G-code for a
|
||||
sortable fill, which is chained again.
|
||||
|
||||
## Connecting the lines
|
||||
|
||||
Most patterns cut their lines at the infill boundary, and `Fill::connect_infill()`
|
||||
joins pairs of line ends that are neighbors on a boundary contour by the arc of
|
||||
wall between them, shortest arcs first, as long as neither end is joined yet and
|
||||
the join closes no loop. An arc is joined when it is shorter than
|
||||
`infill_anchor_max`, or than two anchors and a line width if that is longer:
|
||||
two ends further apart each get an anchor toward the other, and where those
|
||||
would meet, the join prints about as much wall as one path instead of two.
|
||||
|
||||
Regular patterns leave many arcs of the same length, and the order among them
|
||||
decides the pairing. In an arbitrary order the pairs along one wall can fall out
|
||||
of step, such as lines 1–2 and 3–4, then 6–7 and 8–9, which leaves the end of
|
||||
line 5 loose, and every loose end starts or ends a separate path. So the arcs
|
||||
are also sorted in steps of half the line spacing and, within a step, along each
|
||||
contour forwards and backwards, which pairs the ends of a wall in one phase.
|
||||
Each order is replayed on the flags and a union-find of the lines, without
|
||||
building any path, and the one that joins the most lines is used; a tie keeps
|
||||
the shortest-first order.
|
||||
|
||||
The replay includes the last pass, which joins any two open ends that are
|
||||
neighbors on a contour. With multiline infill the main pass leaves the copies of
|
||||
one line to that pass, so that each copy reaches a wall of its own. Two
|
||||
neighboring groups cut at both walls can then be joined at either wall by arcs
|
||||
of the same length: at one, their open ends stay next to their own copies, which
|
||||
the last pass joins, so each line prints out and back; at the other, the copies
|
||||
end up in separate paths printed apart.
|
||||
|
||||
A line end left unpaired gets an anchor, a piece of the wall at most
|
||||
`infill_anchor` long. It runs toward the nearest open end of another path,
|
||||
looking up to 64 ends along the contour, since the next path usually starts
|
||||
there; without one it takes the side with more free wall. With multiline infill
|
||||
an anchor toward a copy of the same line stops short of the copy rather than
|
||||
printing over it. A copy is a neighboring end less than the group width
|
||||
(`multiline` spacings) across the line, at any wall angle, while the next group
|
||||
lies a whole line spacing away. Meeting the wall at an angle θ, the copy covers
|
||||
half a width over sin θ of the wall on each side of its end, so the anchor stops
|
||||
that far and half its own width before it, or is left out; the end still counts
|
||||
as anchored, so the last pass pairs the other ends as before.
|
||||
|
||||
Last, `Fill::fill_surface_extrusion()` joins the sparse infill lines whose ends
|
||||
are at most two line spacings apart with a straight piece. With the anchors off
|
||||
(`infill_anchor_max` 0) nothing above runs and this is the only joining; with
|
||||
them on, it takes what the steps above leave next to each other, such as two
|
||||
anchors that stop just short of each other or a short piece cut off by a corner.
|
||||
A gap that short leaves no room to print anything in between, and as a travel it
|
||||
is the one hop a reordering can trade for a longer travel that retracts.
|
||||
Multiline copies leave the most such ends: one spacing apart across the line,
|
||||
their ends where it meets a wall at an angle θ are one spacing over sin θ apart
|
||||
along it, so two spacings reach walls met at 30° and steeper.
|
||||
|
||||
Ends that meet are joined first, then the gaps between the leading ends of their
|
||||
lines, along each line's overall direction, then the rest, shortest first within
|
||||
each, without closing a loop or crossing a line or another join. Taking the
|
||||
leading ends first joins every pair of copies at the same side, which leaves
|
||||
their open ends next to each other; by length alone, neighboring pairs were
|
||||
joined at different walls and the travel between them crossed the island. Lines
|
||||
that cannot reverse, such as Grid's, only join the end of one to the start of the
|
||||
next, and fills that keep their order are left alone.
|
||||
|
||||
## The search
|
||||
|
||||
A chain of up to eight paths is found exactly, by dynamic programming over the
|
||||
subsets of paths: a state is the paths visited so far, the last one and its
|
||||
direction, about four thousand states for eight paths. The shared greedy
|
||||
chaining, `chain_segments_greedy_constrained_reversals_()`, does the same for two
|
||||
to four segments, which also orders up to four islands of a layer and the loops
|
||||
of a perimeter collection exactly. Either way the first path is no longer
|
||||
always the one nearest the start; with a free end, a path near the start can be
|
||||
worth leaving for last.
|
||||
|
||||
Objects (`print_order` Default) and the tour of islands across objects are
|
||||
visited in the same order on every layer, so `chain_points_with_postprocessing()`
|
||||
orders them as a closed tour rather than an open chain. Up to ten points the
|
||||
shortest tour is found exactly by the same dynamic programming with the first
|
||||
point fixed; with a start point the tour then begins at the point nearest it.
|
||||
Larger sets take the greedy chain and alternate 2-opt, or-opt and crossing
|
||||
removal; or-opt moves runs of up to three points next to one of the eight points
|
||||
nearest an end of the run, found on the same grid as the chain's candidates.
|
||||
|
||||
For longer chains, the search refines the greedy chain. On an island cut into many short pieces,
|
||||
such as infill without anchors, that chain can sweep away from the nozzle and
|
||||
come back for the rest with one long jump, which no local move below removes.
|
||||
So a nearest-next chain, going from the nozzle to the nearest free path end each
|
||||
time, is built too; when it is already shorter than the refined greedy chain, it
|
||||
is refined and used instead. Elsewhere it costs one grid query per path.
|
||||
|
||||
`improve_chain_by_local_search()` sees the chain as a sequence of segments with
|
||||
an entry and an exit point each. Its cost is the straight distance from the
|
||||
nozzle to the first entry plus the distance from every exit to the next entry;
|
||||
the end of the chain is free. It applies three kinds of moves:
|
||||
|
||||
- 2-opt reverses a run of segments, which flips each of them and replaces the
|
||||
two links at the ends of the run.
|
||||
- Or-opt moves a run of up to three segments between two others, flipped or
|
||||
not.
|
||||
- A run of any length that starts or ends next to the nozzle moves to the front.
|
||||
Local search can settle on a chain that starts with a block of paths far from
|
||||
the nozzle while the paths next to it sit in the middle, and moving that block
|
||||
takes more than three segments at once.
|
||||
|
||||
A move that would flip a segment that cannot be reversed is skipped, so such
|
||||
segments are only reordered.
|
||||
|
||||
A move is tried only if every link it creates joins an end point to one of the
|
||||
32 end points nearest to it, which covers every move on a chain of up to 17
|
||||
segments, as most islands are; with 8 candidates the search missed most of the
|
||||
gain on grid-like patterns such as Triangles. The candidates come from a uniform
|
||||
grid of about two end points per cell, searched in rings of cells around the
|
||||
query. `KDTreeIndirect` visits its subtrees in a fixed order rather than the
|
||||
nearer one first, so a query for 32 points walks most of the tree, and on long
|
||||
chains finding the candidates would cost more than the search itself.
|
||||
|
||||
The candidates of an end point are tried nearest first, and only while they are
|
||||
closer to it than the link the move would replace there. A 2-opt move that pays
|
||||
off creates a link shorter than the one it replaces at one of its two ends, so
|
||||
this misses no 2-opt move. An or-opt move is tried only while its new link is
|
||||
shorter than what taking the run out saves, which skips a few or-opt moves that
|
||||
would still pay off but cuts the search on long chains to a fraction.
|
||||
|
||||
The search works through a queue of segments and applies the first move that
|
||||
shortens the chain around the segment it examines. After a move, only the
|
||||
segments next to a changed link are queued again. A new link can also make room
|
||||
for a run elsewhere that no segment next to it would try, so when the queue
|
||||
empties after any move, every segment is queued once more, and the search ends
|
||||
when such a sweep finds no move.
|
||||
|
||||
Every move shortens the chain by more than `SCALED_EPSILON`, so the result is
|
||||
never longer than the greedy or the nearest-next chain and the search ends; a
|
||||
cap of 50 moves per segment only bounds its time. The search has no randomness
|
||||
and visits the segments in a fixed order, so the same input always gives the
|
||||
same G-code.
|
||||
|
||||
## Cost
|
||||
|
||||
Finding the candidates of an end point visits a few cells, and the nearest-next
|
||||
chain makes one such query per path, which only grows where the paths around the
|
||||
nozzle are used up and the chain jumps. Examining a segment tries at most a few
|
||||
hundred candidate links, usually only the nearest few, plus a scan of the chain
|
||||
for the move to the front when it is the first segment, and a move rewrites only
|
||||
the range of the chain it changes, so the search stays close to linear in the
|
||||
number of segments. Most islands hold a few to a few dozen paths; the cost shows
|
||||
on infill cut into thousands of pieces per island, such as dense infill with the
|
||||
anchors off. The straight joins only look at the line segments near a candidate
|
||||
join for crossings.
|
||||
|
||||
## Limitations
|
||||
|
||||
The search minimizes straight-line distance. It does not know which travels
|
||||
leave the infill area and retract under `reduce_infill_retraction`, nor the
|
||||
detours of `reduce_crossing_wall`, and a travel's time is not proportional to
|
||||
its length: a short travel is bound by acceleration, so its time grows about
|
||||
with the square root of its length. A chain that is shorter in total but trades
|
||||
very short travels for longer ones, or for ones past `retraction_minimum_travel`,
|
||||
can occasionally take a little longer to print.
|
||||
|
||||
The end of the chain is free: it is not steered toward the next island or the
|
||||
start of the next layer. The regions of an island are chained one after the
|
||||
other, in region order, each from where the previous one ended.
|
||||
@@ -36839,7 +36839,9 @@ msgstr ""
|
||||
msgid ""
|
||||
"Reverse on even\n"
|
||||
"Did you know that <b>Reverse on even</b> feature can significantly improve the surface quality of your overhangs? However, it can cause wall inconsistencies so use carefully!"
|
||||
msgstr "Реверс на чётных слояхСовет: настройка <b>Реверс на чётных слоях</b> может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью."
|
||||
msgstr ""
|
||||
"Реверс на чётных слоях\n"
|
||||
"Совет: настройка <b>Реверс на чётных слоях</b> может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью."
|
||||
|
||||
#: resources/data/hints.ini: [hint:Cut Tool]
|
||||
msgid ""
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,126 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Fails when a Windows build has an exception class with broken throw info.
|
||||
|
||||
.DESCRIPTION
|
||||
Clang on the MSVC ABI can leave the copy constructor out of the CatchableType it emits for
|
||||
an exception class that comes through a precompiled header
|
||||
(https://github.com/llvm/llvm-project/issues/53486). The linker keeps one throw info per
|
||||
class, so when it keeps such a record, a copy of that exception (std::exception_ptr, a catch
|
||||
by value) shares its data with the original and the data is freed twice.
|
||||
|
||||
A class is either trivially copyable or not, so a binary should never have both a CatchableType
|
||||
without a copy constructor (_CT??_R0?AV<class>@@@8<size>) and one with it
|
||||
(_CT??_R0?AV<class>@@@8??0<class>...). The script lists each PDB's public symbols with the
|
||||
llvm-pdbutil next to the build's compiler and reports every class that has both.
|
||||
|
||||
scripts\test_check_exception_throw_info.ps1 tests it.
|
||||
|
||||
.PARAMETER BuildDir
|
||||
The slicer build directory. Its CMakeCache.txt names the compiler, and its src\<Config>
|
||||
holds the PDBs to check, which must include OrcaSlicer.pdb.
|
||||
|
||||
.PARAMETER Config
|
||||
The configuration whose PDBs to check (default: Release).
|
||||
|
||||
.PARAMETER Pdb
|
||||
PDBs to check instead of the ones in <BuildDir>\src\<Config>, as a list or comma-separated.
|
||||
|
||||
.PARAMETER Dump
|
||||
Saved `llvm-pdbutil dump --publics` output to check instead of PDBs. Needs no build.
|
||||
|
||||
.EXAMPLE
|
||||
scripts\check_exception_throw_info.ps1 -BuildDir build
|
||||
#>
|
||||
param(
|
||||
[string] $BuildDir,
|
||||
[string] $Config = 'Release',
|
||||
[string[]] $Pdb,
|
||||
[string[]] $Dump
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# powershell -File passes "a.pdb,b.pdb" as one string. A path that exists as given stays whole.
|
||||
function Split-PathList([string[]] $values) {
|
||||
@($values | Where-Object { $_ } | ForEach-Object {
|
||||
if ((Test-Path -LiteralPath $_) -or $_ -notmatch ',') { $_ } else { $_ -split ',' | Where-Object { $_ } }
|
||||
})
|
||||
}
|
||||
|
||||
$Dump = Split-PathList $Dump
|
||||
if (-not $Dump) {
|
||||
if (-not $BuildDir) {
|
||||
throw 'Name the slicer build directory with -BuildDir.'
|
||||
}
|
||||
$cache = Join-Path $BuildDir 'CMakeCache.txt'
|
||||
if (-not (Test-Path $cache)) {
|
||||
throw "No CMakeCache.txt in $BuildDir. Name the slicer build directory."
|
||||
}
|
||||
$entry = Select-String -Path $cache -Pattern '^CMAKE_CXX_COMPILER:\w+=(.+)$' | Select-Object -First 1
|
||||
if (-not $entry) {
|
||||
throw "No CMAKE_CXX_COMPILER in $cache."
|
||||
}
|
||||
$compiler = $entry.Matches[0].Groups[1].Value
|
||||
# A tree configured with a bare compiler name found it on PATH.
|
||||
if (-not (Split-Path $compiler)) {
|
||||
$found = Get-Command $compiler -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $found) {
|
||||
throw "$compiler, the compiler in $cache, is not on PATH."
|
||||
}
|
||||
$compiler = $found.Source
|
||||
}
|
||||
$pdbutil = Join-Path (Split-Path $compiler) 'llvm-pdbutil.exe'
|
||||
if (-not (Test-Path $pdbutil)) {
|
||||
throw "No llvm-pdbutil.exe next to $compiler. This check needs a build made with Clang."
|
||||
}
|
||||
|
||||
$Pdb = Split-PathList $Pdb
|
||||
if (-not $Pdb) {
|
||||
# OrcaSlicer.dll holds the slicer itself, so a check without it proves nothing.
|
||||
$pdbDir = Join-Path $BuildDir "src\$Config"
|
||||
if (-not (Test-Path (Join-Path $pdbDir 'OrcaSlicer.pdb'))) {
|
||||
throw "No OrcaSlicer.pdb in $pdbDir."
|
||||
}
|
||||
$Pdb = @(Get-ChildItem $pdbDir -Filter *.pdb | ForEach-Object FullName)
|
||||
}
|
||||
}
|
||||
|
||||
$catchable = [regex]'`_CT\?\?_R0\?A[UV](.+?)@@@8([^`]*)`'
|
||||
$tmp = [IO.Path]::GetTempFileName()
|
||||
$failed = $false
|
||||
$classes = 0
|
||||
try {
|
||||
foreach ($file in $(if ($Dump) { $Dump } else { $Pdb })) {
|
||||
$text = $file
|
||||
if (-not $Dump) {
|
||||
$run = Start-Process -FilePath $pdbutil -ArgumentList 'dump', '--publics', "`"$file`"" -NoNewWindow -Wait -PassThru -RedirectStandardOutput $tmp
|
||||
if ($run.ExitCode -ne 0) {
|
||||
throw "llvm-pdbutil failed on $file."
|
||||
}
|
||||
$text = $tmp
|
||||
}
|
||||
|
||||
$kinds = @{}
|
||||
foreach ($match in $catchable.Matches([IO.File]::ReadAllText((Resolve-Path -LiteralPath $text).Path))) {
|
||||
$class = $match.Groups[1].Value
|
||||
if (-not $kinds.ContainsKey($class)) { $kinds[$class] = @{} }
|
||||
$kinds[$class][$(if ($match.Groups[2].Value.StartsWith('??0')) { 'copy' } else { 'bitwise' })] = $true
|
||||
}
|
||||
|
||||
$broken = @($kinds.Keys | Where-Object { $kinds[$_].Count -eq 2 } | Sort-Object)
|
||||
"$(Split-Path -Leaf $file): $($kinds.Count) exception classes, $($broken.Count) with broken throw info"
|
||||
foreach ($class in $broken) { " $class" }
|
||||
if ($broken.Count -gt 0) { $failed = $true }
|
||||
$classes += $kinds.Count
|
||||
}
|
||||
} finally {
|
||||
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# The slicer's PDBs always contain exception throw info, so finding none means the dump format or the PDBs changed.
|
||||
if ($classes -eq 0) {
|
||||
'No exception classes found. Check that llvm-pdbutil still prints public symbols in backticks.'
|
||||
$failed = $true
|
||||
}
|
||||
|
||||
exit [int]$failed
|
||||
@@ -301,6 +301,10 @@ $cases = @(
|
||||
Contains = @('Precompiled header: off') }
|
||||
@{ Name = 'the precompiled header is on unless asked'; Args = @('-s')
|
||||
NotContains = @('SLIC3R_PCH') }
|
||||
@{ Name = '-l turns the precompiled header off'; Args = @('-s', '-l')
|
||||
Contains = @('-DSLIC3R_PCH=OFF', 'Precompiled header: off') }
|
||||
@{ Name = 'a deps-only build says nothing about the precompiled header'; Args = @('-d', '-l')
|
||||
NotContains = @('Precompiled header') }
|
||||
@{ Name = '--no-pch works without a cache'; Args = @('-s', '--no-pch')
|
||||
NotContains = @('COMPILER_LAUNCHER') }
|
||||
@{ Name = 'the slicer build runs gettext'; Args = @('-s')
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Tests scripts/check_exception_throw_info.ps1 on canned llvm-pdbutil dumps.
|
||||
|
||||
.DESCRIPTION
|
||||
Each case runs the check in its own powershell -File process, as CI does, and asserts on the
|
||||
exit code and output. Dumps are written to a temporary folder, so no build is needed.
|
||||
|
||||
.EXAMPLE
|
||||
powershell -File scripts/test_check_exception_throw_info.ps1
|
||||
#>
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$check = Join-Path $PSScriptRoot 'check_exception_throw_info.ps1'
|
||||
$work = Join-Path ([IO.Path]::GetTempPath()) 'check_exception_throw_info_tests'
|
||||
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $work | Out-Null
|
||||
|
||||
function Write-Dump([string] $path, [string[]] $symbols) {
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $path) | Out-Null
|
||||
$i = 0
|
||||
$lines = @(' Public Symbols', '============================================================')
|
||||
foreach ($symbol in $symbols) {
|
||||
$i++
|
||||
$lines += " $i | S_PUB32 [size = 48] ``$symbol``", ' flags = none, addr = 0007:0000'
|
||||
}
|
||||
Set-Content -Path $path -Encoding ascii -Value $lines
|
||||
}
|
||||
|
||||
$bitwise = '_CT??_R0?AVfuture_error@std@@@840'
|
||||
$copy = '_CT??_R0?AVfuture_error@std@@@8??0future_error@std@@QEAA@AEBV01@@Z40'
|
||||
$trivial = '_CT??_R0?AUtag@@@81'
|
||||
Write-Dump "$work\broken.txt" @($bitwise, $copy, $trivial)
|
||||
Write-Dump "$work\clean.txt" @($copy, $trivial)
|
||||
Write-Dump "$work\empty.txt" @()
|
||||
Write-Dump "$work\a dir, with a comma\clean.txt" @($copy)
|
||||
|
||||
# A build directory whose compiler has an llvm-pdbutil beside it but no OrcaSlicer.pdb.
|
||||
# Nothing runs the stand-in; the check stops before it gets there.
|
||||
$build = "$work\build"
|
||||
New-Item -ItemType Directory -Force -Path "$build\bin", "$build\src\Release" | Out-Null
|
||||
Copy-Item "$env:SystemRoot\System32\where.exe" "$build\bin\llvm-pdbutil.exe"
|
||||
Set-Content -Path "$build\CMakeCache.txt" -Encoding ascii -Value "CMAKE_CXX_COMPILER:FILEPATH=$build\bin\clang-cl.exe"
|
||||
Set-Content -Path "$build\src\Release\orca-slicer.pdb" -Value ''
|
||||
$bare = "$work\bare"
|
||||
New-Item -ItemType Directory -Force -Path $bare | Out-Null
|
||||
Set-Content -Path "$bare\CMakeCache.txt" -Encoding ascii -Value 'CMAKE_CXX_COMPILER:STRING=no-such-clang-cl.exe'
|
||||
|
||||
$cases = @(
|
||||
@{ Name = 'a class with and without a copy constructor fails'; Args = @('-Dump', "$work\broken.txt")
|
||||
Exit = 1; Contains = @('1 with broken throw info', 'future_error@std') }
|
||||
@{ Name = 'copy constructors and trivially copyable classes pass'; Args = @('-Dump', "$work\clean.txt")
|
||||
Exit = 0; Contains = @('2 exception classes, 0 with broken throw info') }
|
||||
@{ Name = 'no exception classes at all fails'; Args = @('-Dump', "$work\empty.txt")
|
||||
Exit = 1; Contains = @('No exception classes found') }
|
||||
@{ Name = 'a comma-separated list checks each dump'; Args = @('-Dump', "$work\broken.txt,$work\clean.txt")
|
||||
Exit = 1; Contains = @('broken.txt: 2 exception classes', 'clean.txt: 2 exception classes') }
|
||||
@{ Name = 'a path with a comma stays whole'; Args = @('-Dump', "$work\a dir, with a comma\clean.txt")
|
||||
Exit = 0; Contains = @('clean.txt: 1 exception classes') }
|
||||
@{ Name = 'a build without OrcaSlicer.pdb fails'; Args = @('-BuildDir', $build)
|
||||
Exit = 1; Contains = @('No OrcaSlicer.pdb in') }
|
||||
@{ Name = 'a compiler that is not on PATH is named'; Args = @('-BuildDir', $bare)
|
||||
Exit = 1; Contains = @('no-such-clang-cl.exe, the compiler in') }
|
||||
@{ Name = 'a missing build directory is named'; Args = @('-BuildDir', "$work\nowhere")
|
||||
Exit = 1; Contains = @('No CMakeCache.txt in') }
|
||||
@{ Name = 'no build directory and no dump fails'; Args = @()
|
||||
Exit = 1; Contains = @('Name the slicer build directory') }
|
||||
)
|
||||
|
||||
$failed = 0
|
||||
foreach ($case in $cases) {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$output = (& powershell -NoProfile -File $check @($case.Args) 2>&1 | Out-String)
|
||||
$code = $LASTEXITCODE
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$problems = @()
|
||||
if ($code -ne $case.Exit) { $problems += "exit $code, expected $($case.Exit)" }
|
||||
foreach ($text in $case.Contains) {
|
||||
if (-not $output.Contains($text)) { $problems += "no '$text' in the output" }
|
||||
}
|
||||
if ($problems) {
|
||||
$failed++
|
||||
" FAIL $($case.Name)"
|
||||
$problems | ForEach-Object { " $_" }
|
||||
$output.TrimEnd() -split "`r?`n" | ForEach-Object { " | $_" }
|
||||
} else {
|
||||
" ok $($case.Name)"
|
||||
}
|
||||
}
|
||||
|
||||
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
|
||||
"$($cases.Count - $failed) passed, $failed failed"
|
||||
exit [int]($failed -gt 0)
|
||||
@@ -666,6 +666,9 @@ endif ()
|
||||
encoding_check(libslic3r)
|
||||
|
||||
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
|
||||
# CGAL changes Kd_tree's member layout when TBB is enabled. Match libslic3r_cgal
|
||||
# in every consumer so inline constructors/destructors cannot mix the two layouts.
|
||||
target_compile_definitions(libslic3r PUBLIC CGAL_LINKED_WITH_TBB)
|
||||
if (USE_SLIC3R_CONSOLE_LOG)
|
||||
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
|
||||
endif()
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#include <numeric>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
@@ -802,72 +800,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
|
||||
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); }
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); }
|
||||
|
||||
namespace ClipperUtils {
|
||||
std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile)
|
||||
{
|
||||
BoundingBox extent;
|
||||
std::vector<BoundingBox> bboxes;
|
||||
bboxes.reserve(expolygons.size());
|
||||
for (const ExPolygon &expoly : expolygons) {
|
||||
bboxes.emplace_back(get_extents(expoly));
|
||||
extent.merge(bboxes.back());
|
||||
}
|
||||
if (! extent.defined)
|
||||
return {};
|
||||
const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max<size_t>(per_tile, 1)))), 1, 32);
|
||||
const Point size = extent.size();
|
||||
const coord_t tile_w = std::max<coord_t>(1, size.x() / tiles + 1), tile_h = std::max<coord_t>(1, size.y() / tiles + 1);
|
||||
std::vector<ExPolygonsTile> out(size_t(tiles * tiles));
|
||||
for (size_t i = 0; i < expolygons.size(); ++ i) {
|
||||
const Point c = bboxes[i].center();
|
||||
ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles +
|
||||
std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))];
|
||||
tile.members.emplace_back(i);
|
||||
tile.bbox.merge(bboxes[i]);
|
||||
}
|
||||
out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end());
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{
|
||||
// A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box.
|
||||
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(subject, 32);
|
||||
// One tile is the plain call: cutting the clip would only cost time.
|
||||
if (tiles.size() <= 1)
|
||||
return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset);
|
||||
std::vector<BoundingBox> clip_bboxes;
|
||||
clip_bboxes.reserve(clip.size());
|
||||
for (const Polygon &polygon : clip)
|
||||
clip_bboxes.emplace_back(get_extents(polygon));
|
||||
|
||||
std::vector<Slic3r::ExPolygons> out_tiles(tiles.size());
|
||||
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
|
||||
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
|
||||
Slic3r::ExPolygons local_subject;
|
||||
local_subject.reserve(tile.members.size());
|
||||
for (size_t i : tile.members)
|
||||
local_subject.emplace_back(subject[i]);
|
||||
// Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset.
|
||||
const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON);
|
||||
Polygons local_clip;
|
||||
for (size_t i = 0; i < clip.size(); ++i)
|
||||
if (clip_bboxes[i].overlap(bbox))
|
||||
if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty())
|
||||
local_clip.emplace_back(std::move(clipped));
|
||||
out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset);
|
||||
});
|
||||
Slic3r::ExPolygons out;
|
||||
for (Slic3r::ExPolygons &out_tile : out_tiles)
|
||||
append(out, std::move(out_tile));
|
||||
return out;
|
||||
}
|
||||
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{ return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); }
|
||||
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
|
||||
{ return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); }
|
||||
// May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative).
|
||||
Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
|
||||
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); }
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "Polyline.hpp"
|
||||
#include "Line.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Surface.hpp"
|
||||
@@ -333,15 +332,6 @@ namespace ClipperUtils {
|
||||
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
|
||||
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
|
||||
|
||||
// Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a
|
||||
// layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box.
|
||||
struct ExPolygonsTile
|
||||
{
|
||||
BoundingBox bbox;
|
||||
std::vector<size_t> members;
|
||||
};
|
||||
[[nodiscard]] std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile);
|
||||
|
||||
}
|
||||
|
||||
// offset Polygons
|
||||
@@ -537,11 +527,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
// diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in
|
||||
// parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a
|
||||
// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line.
|
||||
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
|
||||
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
|
||||
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip);
|
||||
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
|
||||
|
||||
@@ -616,7 +616,7 @@ struct PropagateDanielssonSingleStep {
|
||||
if ((signs[addr] & 2) == 0) {
|
||||
float *v = &L[addr << 1];
|
||||
float l = v[0] * v[0] + v[1] * v[1];
|
||||
float *v2s = v + (addr_delta << 1);
|
||||
float *v2s = v + 2 * addr_delta;
|
||||
float v2[2] = {
|
||||
v2s[0] + INCX * resolution,
|
||||
v2s[1] + INCY * resolution
|
||||
|
||||
+12
-31
@@ -22,8 +22,6 @@
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../Surface.hpp"
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include "AABBTreeLines.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
@@ -677,28 +675,24 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
|
||||
if (!line_based_pattern) {
|
||||
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing);
|
||||
|
||||
// Each expolygon is split on its own, so they run in parallel and are collected in their original order.
|
||||
std::vector<std::pair<ExPolygons, ExPolygons>> split_parts(fill.expolygons.size()); // normal, narrow
|
||||
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t idx) {
|
||||
const ExPolygon &expolygon = fill.expolygons[idx];
|
||||
for (const ExPolygon &expolygon : fill.expolygons) {
|
||||
Polygons filled_area = to_polygons(expolygon);
|
||||
|
||||
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon.
|
||||
Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
|
||||
|
||||
if (inner_area.empty()) {
|
||||
split_parts[idx].second.emplace_back(expolygon);
|
||||
return;
|
||||
narrow_infill.emplace_back(expolygon);
|
||||
continue;
|
||||
}
|
||||
|
||||
ExPolygons inner_ex = union_ex(inner_area);
|
||||
ExPolygons expolys{expolygon};
|
||||
split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
|
||||
split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
|
||||
});
|
||||
for (auto &[normal_ex, narrow_ex] : split_parts) {
|
||||
append(normal_infill, std::move(normal_ex));
|
||||
append(narrow_infill, std::move(narrow_ex));
|
||||
ExPolygons narrow_ex = diff_ex(expolys, inner_ex);
|
||||
ExPolygons normal_ex = intersection_ex(expolys, inner_ex);
|
||||
|
||||
append(normal_infill, normal_ex); // normal infill area
|
||||
append(narrow_infill, narrow_ex); // narrow infill area
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -720,10 +714,7 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
|
||||
}
|
||||
const double aligning_angle = -base_angle + PI;
|
||||
|
||||
// Each expolygon is reconstructed on its own, so they run in parallel and are collected in their original order.
|
||||
std::vector<Polygons> split_reconstructed(fill.expolygons.size());
|
||||
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t expolygon_idx) {
|
||||
const ExPolygon &expolygon = fill.expolygons[expolygon_idx];
|
||||
for (const ExPolygon &expolygon : fill.expolygons) {
|
||||
Polygons filled_area = to_polygons(expolygon);
|
||||
polygons_rotate(filled_area, aligning_angle);
|
||||
BoundingBox bb = get_extents(filled_area);
|
||||
@@ -854,10 +845,8 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
|
||||
}
|
||||
}
|
||||
|
||||
split_reconstructed[expolygon_idx] = std::move(reconstructed_area);
|
||||
});
|
||||
for (Polygons &reconstructed_area : split_reconstructed)
|
||||
polygons_append(normal_fill_areas, std::move(reconstructed_area));
|
||||
polygons_append(normal_fill_areas, reconstructed_area);
|
||||
}
|
||||
|
||||
polygons_rotate(normal_fill_areas, -aligning_angle);
|
||||
|
||||
@@ -1484,15 +1473,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
|
||||
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
|
||||
|
||||
// Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the
|
||||
// expolygon's box first (padded past the safety offset, which grows the clip side). The result is
|
||||
// identical; the cost is not: a layer split into many small fills, e.g. by colour painting,
|
||||
// otherwise intersects every one of them with the whole layer.
|
||||
BoundingBox no_overlap_bbox = get_extents(expoly);
|
||||
no_overlap_bbox.offset(SCALED_EPSILON);
|
||||
f->no_overlap_expolygons = intersection_ex(
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox),
|
||||
ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
if (params.symmetric_infill_y_axis) {
|
||||
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
|
||||
expoly.symmetric_y(params.symmetric_y_axis);
|
||||
|
||||
+256
-13
@@ -11,6 +11,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <tuple>
|
||||
#include "../AABBTreeLines.hpp"
|
||||
#include "../ClipperUtils.hpp"
|
||||
#include "../EdgeGrid.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
@@ -206,6 +210,9 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
|
||||
thick_polylines.clear();
|
||||
}
|
||||
else {
|
||||
// Orca: sparse infill lines whose ends are next to each other print as one path, without a travel between them.
|
||||
if (params.extrusion_role == erInternalInfill && ! eec->no_sort)
|
||||
join_close_ends(polylines, scale_(2. * this->spacing), params.can_reverse);
|
||||
extrusion_entities_append_paths(
|
||||
eec->entities, std::move(polylines),
|
||||
params.extrusion_role,
|
||||
@@ -1644,6 +1651,8 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
};
|
||||
|
||||
const double line_half_width = 0.5 * scale_(spacing);
|
||||
// Two ends whose anchors would meet are joined instead, with about as much wall.
|
||||
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
|
||||
|
||||
#if 0
|
||||
// Connection from end of one infill line to the start of another infill line.
|
||||
@@ -1720,6 +1729,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
#endif
|
||||
|
||||
// Line ends not yet joined to another line along the contour.
|
||||
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
|
||||
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
|
||||
|
||||
struct Arc {
|
||||
ContourIntersectionPoint *intersection;
|
||||
double arc_length;
|
||||
@@ -1731,6 +1744,81 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
|
||||
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
|
||||
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
|
||||
|
||||
// Lines the arcs would join in this order.
|
||||
auto count_joins = [&](const std::vector<Arc> &order) {
|
||||
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
|
||||
for (size_t i = 0; i < consumed.size(); ++ i)
|
||||
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
|
||||
std::vector<size_t> parent(infill_ordered.size());
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
size_t joins = 0;
|
||||
for (const Arc &arc : order) {
|
||||
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
|
||||
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
|
||||
continue;
|
||||
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
if (arc.arc_length < join_length) {
|
||||
parent[polyline2] = polyline1;
|
||||
++ joins;
|
||||
} else if (anchor_length <= SCALED_EPSILON)
|
||||
continue;
|
||||
consumed[end1] = consumed[end2] = 1;
|
||||
}
|
||||
// The last pass joins the open ends next to each other, also those of a multiline group.
|
||||
for (size_t end = 0; end < consumed.size(); ++ end) {
|
||||
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
|
||||
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
|
||||
continue;
|
||||
const double length = graph.boundary_params[cp.contour_idx].back();
|
||||
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
|
||||
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
|
||||
};
|
||||
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
|
||||
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
|
||||
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
|
||||
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
|
||||
consumed[end] = anchor_length > SCALED_EPSILON;
|
||||
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l > join_length)
|
||||
break;
|
||||
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
|
||||
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
parent[polyline2] = polyline1;
|
||||
consumed[end] = consumed[other] = 1;
|
||||
++ joins;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return joins;
|
||||
};
|
||||
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
|
||||
size_t best_joins = count_joins(arches);
|
||||
const double bucket = 0.5 * scale_(spacing);
|
||||
for (const bool backwards : { false, true }) {
|
||||
std::vector<Arc> along_contour = arches;
|
||||
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
|
||||
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
|
||||
if (bucket_l != bucket_r)
|
||||
return bucket_l < bucket_r;
|
||||
if (l.intersection->contour_idx != r.intersection->contour_idx)
|
||||
return l.intersection->contour_idx < r.intersection->contour_idx;
|
||||
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
|
||||
});
|
||||
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
|
||||
best_joins = joins;
|
||||
arches = std::move(along_contour);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
|
||||
@@ -1752,7 +1840,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (polyline_idx1 != polyline_idx2) {
|
||||
Polyline &polyline1 = infill_ordered[polyline_idx1];
|
||||
Polyline &polyline2 = infill_ordered[polyline_idx2];
|
||||
if (arc.arc_length < anchor_length_max) {
|
||||
if (arc.arc_length < join_length) {
|
||||
// Not closing a loop, connecting the lines.
|
||||
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
|
||||
if (contour[cp1->point_idx] == polyline1.points.front())
|
||||
@@ -1761,6 +1849,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline1, polyline2, contour, cp1, cp2, false);
|
||||
open_end[end_index(cp1)] = open_end[end_index(cp2)] = 0;
|
||||
// Mark the second polygon as merged with the first one.
|
||||
if (polyline_idx2 < polyline_idx1) {
|
||||
polyline2 = std::move(polyline1);
|
||||
@@ -1778,6 +1867,23 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
}
|
||||
|
||||
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
|
||||
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
|
||||
const double length = graph.boundary_params[from.contour_idx].back();
|
||||
double distance = 0.;
|
||||
const ContourIntersectionPoint *cp = &from;
|
||||
for (size_t step = 0; step < 64; ++ step) {
|
||||
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
|
||||
if (other == nullptr || other == &from)
|
||||
break;
|
||||
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
|
||||
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
|
||||
return distance;
|
||||
cp = other;
|
||||
}
|
||||
return std::numeric_limits<double>::max();
|
||||
};
|
||||
|
||||
// Connect the remaining open infill lines to the perimeter lines if possible.
|
||||
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
|
||||
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
|
||||
@@ -1796,7 +1902,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
|
||||
bool connected = false;
|
||||
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l == std::numeric_limits<double>::max() || l > anchor_length_max)
|
||||
if (l == std::numeric_limits<double>::max() || l > join_length)
|
||||
break;
|
||||
// Take the complete contour.
|
||||
bool reversed = l == lprev;
|
||||
@@ -1815,6 +1921,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline, polyline2, contour, &contour_point, cp2, reversed);
|
||||
open_end[end_index(&contour_point)] = open_end[end_index(cp2)] = 0;
|
||||
if (polyline_idx < polyline_idx2) {
|
||||
// Mark the second polyline as merged with the first one.
|
||||
merged_with[polyline_idx2] = polyline_idx;
|
||||
@@ -1829,18 +1936,35 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
break;
|
||||
}
|
||||
if (! connected && anchor_length > SCALED_EPSILON) {
|
||||
// Which to take? One could optimize for:
|
||||
// 1) Shortest path
|
||||
// 2) Hook length
|
||||
// ...
|
||||
// Let's take the longer now, as this improves the chance of another hook to be placed on the other side of this contour point.
|
||||
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next);
|
||||
if (l > SCALED_EPSILON) {
|
||||
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width);
|
||||
else
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width);
|
||||
// Hook toward the nearest open end of another polyline, where the next path likely starts.
|
||||
const double free_prev = contour_point.contour_not_taken_length_prev;
|
||||
const double free_next = contour_point.contour_not_taken_length_next;
|
||||
const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
|
||||
const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
|
||||
bool backwards = free_prev > free_next;
|
||||
if (to_prev < to_next && free_prev > SCALED_EPSILON)
|
||||
backwards = true;
|
||||
else if (to_next < to_prev && free_next > SCALED_EPSILON)
|
||||
backwards = false;
|
||||
// Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
|
||||
double length = anchor_length;
|
||||
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
|
||||
params.multiline > 1 && other != &contour_point) {
|
||||
const Point &end = contour[contour_point.point_idx];
|
||||
Vec2d dir = Vec2d::Zero();
|
||||
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
|
||||
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
|
||||
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
|
||||
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
|
||||
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
|
||||
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
|
||||
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
|
||||
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
|
||||
}
|
||||
}
|
||||
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
|
||||
backwards, std::max(0., length), line_half_width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2798,4 +2922,123 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
|
||||
polylines = std::move(all_polylines);
|
||||
}
|
||||
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
|
||||
{
|
||||
const size_t n = polylines.size();
|
||||
if (n < 2 || max_gap <= 0.)
|
||||
return;
|
||||
static constexpr size_t npos = size_t(-1);
|
||||
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
|
||||
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
|
||||
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
|
||||
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
|
||||
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
|
||||
std::unordered_map<uint64_t, std::vector<size_t>> grid;
|
||||
for (size_t e = 0; e < 2 * n; ++ e)
|
||||
if (polylines[e >> 1].size() >= 2)
|
||||
grid[key(cell_of(end_point(e)))].push_back(e);
|
||||
|
||||
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
|
||||
std::vector<char> backwards(n, 0);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (polylines[i].size() >= 2) {
|
||||
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
|
||||
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
|
||||
}
|
||||
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
|
||||
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
|
||||
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
|
||||
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
|
||||
for (const auto &[cell_key, ends] : grid) {
|
||||
const Vec2i64 c = cell_of(end_point(ends.front()));
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t a : ends)
|
||||
for (size_t b : it->second)
|
||||
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
|
||||
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
|
||||
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
|
||||
}
|
||||
if (gaps.empty())
|
||||
return;
|
||||
std::sort(gaps.begin(), gaps.end());
|
||||
|
||||
std::vector<size_t> partner(2 * n, npos), parent(n);
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
|
||||
// Only the segments through a cell next to a candidate join can cross one.
|
||||
std::unordered_set<uint64_t> near_gaps;
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
for (size_t e : { a, b })
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
|
||||
Lines near_lines;
|
||||
for (const Polyline &pl : polylines)
|
||||
for (size_t i = 1; i < pl.size(); ++ i) {
|
||||
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
|
||||
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
|
||||
bool near = (hi - lo).maxCoeff() > 3;
|
||||
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
|
||||
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
|
||||
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
|
||||
if (near)
|
||||
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
|
||||
}
|
||||
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
|
||||
auto crosses = [&](size_t a, size_t b) {
|
||||
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
|
||||
const double length = (q - p).norm();
|
||||
if (length <= 2. * SCALED_EPSILON)
|
||||
return false;
|
||||
const Vec2d dir = (q - p) / length;
|
||||
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
|
||||
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
|
||||
return true;
|
||||
const Vec2i64 c = cell_of(end_point(a));
|
||||
for (int64_t dx = -3; dx <= 3; ++ dx)
|
||||
for (int64_t dy = -3; dy <= 3; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t e : it->second)
|
||||
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
|
||||
partner[a] = b;
|
||||
partner[b] = a;
|
||||
parent[find(a >> 1)] = find(b >> 1);
|
||||
}
|
||||
|
||||
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
|
||||
Polylines out;
|
||||
std::vector<char> done(n, 0);
|
||||
auto walk = [&](size_t first_end) {
|
||||
Polyline joined;
|
||||
for (size_t e = first_end; e != npos;) {
|
||||
Polyline &pl = polylines[e >> 1];
|
||||
done[e >> 1] = 1;
|
||||
if (e & 1)
|
||||
pl.reverse();
|
||||
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
|
||||
e = partner[e ^ 1];
|
||||
}
|
||||
out.emplace_back(std::move(joined));
|
||||
};
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i] == npos)
|
||||
walk(2 * i);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i + 1] == npos)
|
||||
walk(2 * i + 1);
|
||||
polylines = std::move(out);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -258,8 +258,11 @@ public:
|
||||
|
||||
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance);
|
||||
};
|
||||
//Fill Multiline
|
||||
//Fill Multiline
|
||||
void multiline_fill(Polylines& polylines, const FillParams& params, float spacing);
|
||||
// Joins polylines whose ends are at most max_gap apart, shortest gaps first and without closing a loop, so each such
|
||||
// group prints as one path. Without can_reverse only the end of one polyline joins the start of another.
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse);
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_FillBase_hpp_
|
||||
@@ -6660,10 +6660,6 @@ LayerResult GCode::process_layer(
|
||||
case CalibMode::Calib_Input_shaping_freq: {
|
||||
if (m_layer_index == 1){
|
||||
gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type);
|
||||
if (m_writer.get_gcode_flavor() == gcfKlipper) {
|
||||
// Disable minimum cruise ratio to ensure consistent motion for calibration
|
||||
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
|
||||
}
|
||||
} else {
|
||||
if (print.calib_params().freqStartX == print.calib_params().freqStartY && print.calib_params().freqEndX == print.calib_params().freqEndY) {
|
||||
gcode += writer().set_input_shaping('A', 0.f, this->interpolate_value_across_layers(print.calib_params().freqStartX, print.calib_params().freqEndX), "");
|
||||
@@ -6676,10 +6672,6 @@ LayerResult GCode::process_layer(
|
||||
}
|
||||
case CalibMode::Calib_Input_shaping_damp: {
|
||||
if (m_layer_index == 1){
|
||||
if (m_writer.get_gcode_flavor() == gcfKlipper) {
|
||||
// Disable minimum cruise ratio to ensure consistent motion for calibration
|
||||
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
|
||||
}
|
||||
gcode += writer().set_input_shaping('X', 0.f, print.calib_params().freqStartX, print.calib_params().shaper_type);
|
||||
gcode += writer().set_input_shaping('Y', 0.f, print.calib_params().freqStartY, print.calib_params().shaper_type);
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/algorithm/string/constants.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -378,6 +379,11 @@ void GCodeProcessor::TimeMachine::reset()
|
||||
travel_acceleration = 0.0f;
|
||||
max_travel_acceleration = 0.0f;
|
||||
extrude_factor_override_percentage = 1.0f;
|
||||
klipper = false;
|
||||
minimum_cruise_ratio = 0.5f;
|
||||
requested_accel_to_decel = -1.0f;
|
||||
klipper_junction_flush = 1.0f;
|
||||
klipper_queue_priming = true;
|
||||
time = 0.0f;
|
||||
stop_times = std::vector<StopTime>();
|
||||
curr.reset();
|
||||
@@ -493,16 +499,87 @@ GCodeProcessor::TimeMachine::AdditionalBuffer GCodeProcessor::TimeMachine::merge
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Klipper toolhead.py LookAheadQueue.flush: the pseudo-acceleration pass
|
||||
// reserves cruising distance across a sequence, not separately in each segment.
|
||||
size_t GCodeProcessor::TimeMachine::plan_klipper(bool lazy)
|
||||
{
|
||||
klipper_junction_flush = requested_accel_to_decel >= 0.0f ? 0.250f : 0.150f;
|
||||
klipper_queue_priming = false;
|
||||
size_t flush_count = blocks.size();
|
||||
bool update_flush_count = lazy;
|
||||
float next_start_v2 = 0.0f;
|
||||
float next_mcr_start_v2 = 0.0f;
|
||||
float peak_cruise_v2 = 0.0f;
|
||||
size_t pending = 0;
|
||||
for (size_t i = blocks.size(); i-- > 0;) {
|
||||
TimeBlock& block = blocks[i];
|
||||
const float delta_v2 = 2.0f * block.distance * block.acceleration;
|
||||
const float reachable_start_v2 = next_start_v2 + delta_v2;
|
||||
const float start_v2 = std::min(sqr(block.max_entry_speed), reachable_start_v2);
|
||||
const float reachable_mcr_v2 = next_mcr_start_v2 + block.mcr_delta_v2;
|
||||
const float mcr_start_v2 = std::min(block.max_mcr_entry_speed_sqr, reachable_mcr_v2);
|
||||
float cruise_v2 = -1.0f;
|
||||
++pending;
|
||||
if (mcr_start_v2 < reachable_mcr_v2) {
|
||||
if (mcr_start_v2 + block.mcr_delta_v2 > next_mcr_start_v2 || pending > 1) {
|
||||
// Stop at this block, not past it: the blocks above still have a cruise speed
|
||||
// that depends on moves not yet seen, so only [0, i) may be committed.
|
||||
if (update_flush_count && peak_cruise_v2 > 0.0f) {
|
||||
flush_count = i;
|
||||
update_flush_count = false;
|
||||
}
|
||||
peak_cruise_v2 = std::min(sqr(block.feedrate_profile.cruise),
|
||||
0.5f * (mcr_start_v2 + reachable_mcr_v2));
|
||||
}
|
||||
cruise_v2 = std::min({0.5f * (start_v2 + reachable_start_v2),
|
||||
sqr(block.feedrate_profile.cruise), peak_cruise_v2});
|
||||
pending = 0;
|
||||
}
|
||||
// Until the forward pass, these fields hold v^2, not speeds; -1 marks
|
||||
// an unassigned cruise speed. A lazy return leaves them in this state.
|
||||
block.feedrate_profile.entry = start_v2;
|
||||
block.feedrate_profile.exit = next_start_v2;
|
||||
block.trapezoid.cruise_feedrate = cruise_v2;
|
||||
block.peak_cruise_v2 = peak_cruise_v2;
|
||||
next_start_v2 = start_v2;
|
||||
next_mcr_start_v2 = mcr_start_v2;
|
||||
}
|
||||
if (update_flush_count)
|
||||
return 0;
|
||||
// A run of blocks that cannot accelerate is resolved here, once the peak cruise speed of the
|
||||
// accelerating block that precedes it is known. Each one takes a running minimum with its own
|
||||
// entry speed, seeded from that peak rather than from the setter's own clamped cruise speed.
|
||||
float prev_cruise_v2 = 0.0f;
|
||||
for (size_t i = 0; i < flush_count; ++i) {
|
||||
TimeBlock& block = blocks[i];
|
||||
float cruise_v2 = block.trapezoid.cruise_feedrate;
|
||||
if (cruise_v2 < 0.0f) {
|
||||
cruise_v2 = std::min(prev_cruise_v2, block.feedrate_profile.entry);
|
||||
prev_cruise_v2 = cruise_v2;
|
||||
} else
|
||||
prev_cruise_v2 = block.peak_cruise_v2;
|
||||
const float start_v2 = std::min(block.feedrate_profile.entry, cruise_v2);
|
||||
const float end_v2 = std::min(block.feedrate_profile.exit, cruise_v2);
|
||||
// A zero acceleration would put an infinity in the ramp distances; the Marlin helpers guard
|
||||
// their divides the same way and fall back to a pure cruise.
|
||||
const float inv_two_accel = block.acceleration > 0.0f ? 0.5f / block.acceleration : 0.0f;
|
||||
block.feedrate_profile.entry = std::sqrt(start_v2);
|
||||
block.feedrate_profile.exit = std::sqrt(end_v2);
|
||||
block.trapezoid.cruise_feedrate = std::sqrt(cruise_v2);
|
||||
block.trapezoid.accelerate_until = (cruise_v2 - start_v2) * inv_two_accel;
|
||||
block.trapezoid.decelerate_after = block.distance - (cruise_v2 - end_v2) * inv_two_accel;
|
||||
}
|
||||
return flush_count;
|
||||
}
|
||||
|
||||
void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks, float additional_time, EMoveType target_move_type, bool is_final)
|
||||
{
|
||||
if (!enabled)
|
||||
return;
|
||||
// Orca: on the finalization pass, drain extra time that is still buffered (e.g. a
|
||||
// trailing filament change) even with fewer than two blocks queued -- no later pass
|
||||
// exists to attribute it on. Every other pass keeps the original >= 2 requirement,
|
||||
// and an empty buffer keeps today's early-return unchanged (no behavior change).
|
||||
// Drain buffered delays on the final pass, even with no motion left.
|
||||
// A trailing filament change has no later move to receive its time.
|
||||
const bool drain_final = is_final && !m_additional_time_buffer.empty();
|
||||
if (blocks.size() < 2 && !drain_final) {
|
||||
if ((klipper ? blocks.empty() : blocks.size() < 2) && !drain_final) {
|
||||
// Not enough blocks to attribute the extra time to yet; buffer it so it is
|
||||
// applied on a later pass instead of being dropped.
|
||||
if (additional_time > 0.0f)
|
||||
@@ -510,7 +587,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
return;
|
||||
}
|
||||
|
||||
assert(keep_last_n_blocks <= blocks.size());
|
||||
assert(klipper || keep_last_n_blocks <= blocks.size());
|
||||
|
||||
// Merge any previously buffered extra time with this call's extra time. Each
|
||||
// entry is applied, in order, to the first block matching its target move type
|
||||
@@ -520,23 +597,36 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
additional_buffer.emplace_back(target_move_type, additional_time);
|
||||
additional_buffer = merge_adjacent_additional_time_blocks(additional_buffer);
|
||||
|
||||
// reverse_pass
|
||||
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i) {
|
||||
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
|
||||
size_t n_blocks_process;
|
||||
if (klipper) {
|
||||
n_blocks_process = plan_klipper(keep_last_n_blocks != 0);
|
||||
keep_last_n_blocks = blocks.size() - n_blocks_process;
|
||||
} else {
|
||||
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i)
|
||||
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
|
||||
for (size_t i = 0; i + 1 < blocks.size(); ++i)
|
||||
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
|
||||
recalculate_trapezoids(blocks);
|
||||
n_blocks_process = blocks.size() - keep_last_n_blocks;
|
||||
}
|
||||
|
||||
// forward_pass
|
||||
for (size_t i = 0; i + 1 < blocks.size(); ++i) {
|
||||
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
|
||||
}
|
||||
|
||||
recalculate_trapezoids(blocks);
|
||||
|
||||
const size_t n_blocks_process = blocks.size() - keep_last_n_blocks;
|
||||
size_t additional_buffer_idx = 0;
|
||||
for (size_t i = 0; i < n_blocks_process; ++i) {
|
||||
const TimeBlock& block = blocks[i];
|
||||
float block_time = block.time();
|
||||
double motion_time;
|
||||
if (klipper && block.trapezoid.cruise_feedrate > 0.0f) {
|
||||
// Integrate using the planned speeds. Reconstructing the exit speed
|
||||
// from rounded deceleration distance loses precision near a stop.
|
||||
const double cruise = block.trapezoid.cruise_feedrate;
|
||||
motion_time = block.distance / cruise;
|
||||
if (block.acceleration > 0.0f) {
|
||||
const double accel_delta = cruise - block.feedrate_profile.entry;
|
||||
const double decel_delta = cruise - block.feedrate_profile.exit;
|
||||
motion_time += (sqr(accel_delta) + sqr(decel_delta)) / (2.0 * block.acceleration * cruise);
|
||||
}
|
||||
} else {
|
||||
motion_time = block.time();
|
||||
}
|
||||
double block_time = motion_time;
|
||||
if (additional_buffer_idx < additional_buffer.size()) {
|
||||
const EMoveType buf_move_type = additional_buffer[additional_buffer_idx].first;
|
||||
if (buf_move_type == EMoveType::Noop || buf_move_type == block.move_type) {
|
||||
@@ -545,13 +635,13 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
}
|
||||
}
|
||||
|
||||
time += double(block_time);
|
||||
time += block_time;
|
||||
// Orca: accumulate per-SkipType time spent inside SKIPPABLE regions, folded into this single
|
||||
// calculate_time write site. Fires fleet-wide (the shipping time_lapse_gcode template stamps
|
||||
// blocks stTimelapse), but writes only skippable_part_time, which has no g-code-emitting
|
||||
// reader until the pre-heat injector consumes it, so output is byte-identical.
|
||||
if (block.skippable_type != SkipType::stNone)
|
||||
result.skippable_part_time[block.skippable_type] += block.time();
|
||||
result.skippable_part_time[block.skippable_type] += motion_time;
|
||||
result.moves[block.move_id].time[static_cast<size_t>(mode)] = block_time;
|
||||
gcode_time.cache += block_time;
|
||||
//BBS
|
||||
@@ -689,14 +779,12 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
if (keep_last_n_blocks) {
|
||||
blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process);
|
||||
|
||||
// Ensure that the new first block's entry speed will be preserved to prevent discontinuity
|
||||
// between the erased blocks' exit speed and the new first block's entry speed.
|
||||
// Otherwise, the first block's entry speed could be recalculated on the next pass without
|
||||
// considering that there are no more blocks before this first block. This could lead
|
||||
// to discontinuity between the exit speed (of already processed blocks) and the entry
|
||||
// speed of the first block.
|
||||
TimeBlock &first_block = blocks.front();
|
||||
first_block.max_entry_speed = first_block.feedrate_profile.entry;
|
||||
// Preserve the boundary speed in the Marlin planner. Klipper only flushes
|
||||
// moves whose cruise speed is already independent of future moves.
|
||||
if (!klipper) {
|
||||
TimeBlock& first_block = blocks.front();
|
||||
first_block.max_entry_speed = first_block.feedrate_profile.entry;
|
||||
}
|
||||
} else {
|
||||
blocks.clear();
|
||||
}
|
||||
@@ -3872,9 +3960,9 @@ void GCodeProcessor::finalize(bool post_process)
|
||||
update_slice_warnings();
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
|
||||
double GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? float(m_time_processor.machines[static_cast<size_t>(mode)].time) : 0.0f;
|
||||
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast<size_t>(mode)].time : 0.0;
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const
|
||||
@@ -5031,6 +5119,14 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
|
||||
if (line.has_e()) g1_axes[E] = (double)line.e();
|
||||
std::optional<double> g1_feedrate = std::nullopt;
|
||||
if (line.has_f()) g1_feedrate = (double)line.f();
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// Preserve decimal coordinates for short segments without parsing twice.
|
||||
for (unsigned char axis = X; axis <= E; ++axis)
|
||||
if (line.has(static_cast<Axis>(axis)))
|
||||
g1_axes[axis] = line.precise_value(static_cast<Axis>(axis));
|
||||
if (line.has_f())
|
||||
g1_feedrate = line.precise_value(F);
|
||||
}
|
||||
process_G1(g1_axes, g1_feedrate);
|
||||
}
|
||||
|
||||
@@ -5228,7 +5324,7 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
|
||||
//BBS: limite the cruise according to centripetal acceleration
|
||||
//Only need to handle when both prev and curr segment has movement in x-y plane
|
||||
if ((prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
|
||||
if (m_flavor != gcfKlipper && (prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
|
||||
(curr.enter_direction(0) != 0.0f || curr.enter_direction(1) != 0.0f)) {
|
||||
Vec3f v1 = prev.exit_direction;
|
||||
v1(2, 0) = 0.0f;
|
||||
@@ -5252,6 +5348,15 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
}
|
||||
}
|
||||
|
||||
if (m_flavor == gcfKlipper && !is_extrusion_only_move(delta_pos)) {
|
||||
const float max_velocity = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), X, m_machine_config_idx);
|
||||
if (max_velocity > 0.0f)
|
||||
curr.feedrate = std::min(curr.feedrate, max_velocity);
|
||||
}
|
||||
// Klipper applies E limits to retractions and moves without XY motion,
|
||||
// not to positive extrusion along a printing path.
|
||||
const bool limit_extruder = m_flavor != gcfKlipper || delta_pos[E] < 0.0 ||
|
||||
(delta_pos[X] == 0.0 && delta_pos[Y] == 0.0);
|
||||
// calculates block cruise feedrate
|
||||
float min_feedrate_factor = 1.0f;
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
@@ -5260,7 +5365,8 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage;
|
||||
|
||||
curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]);
|
||||
if (curr.abs_axis_feedrate[a] != 0.0f) {
|
||||
if (curr.abs_axis_feedrate[a] != 0.0f && (a != E || limit_extruder) &&
|
||||
(m_flavor != gcfKlipper || a == Z || a == E)) {
|
||||
float axis_max_feedrate = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
|
||||
if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min<float>(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]);
|
||||
}
|
||||
@@ -5277,20 +5383,42 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
}
|
||||
|
||||
// calculates block acceleration
|
||||
float acceleration =
|
||||
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
(is_extrusion_only_move(delta_pos) ?
|
||||
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
|
||||
float acceleration;
|
||||
if (m_flavor == gcfKlipper && is_extrusion_only_move(delta_pos)) {
|
||||
// Klipper's max_extrude_only_accel. machine_max_acceleration_e is optional and some
|
||||
// shipped profiles leave it at 0, so keep the non-zero retract fallback.
|
||||
acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), E, m_machine_config_idx);
|
||||
if (acceleration <= 0.0f)
|
||||
acceleration = get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i));
|
||||
} else
|
||||
acceleration =
|
||||
(m_flavor == gcfKlipper) ? get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
(is_extrusion_only_move(delta_pos) ?
|
||||
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
||||
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
|
||||
|
||||
//BBS
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
if ((a == E && !limit_extruder) || (m_flavor == gcfKlipper && (a == X || a == Y)))
|
||||
continue;
|
||||
float axis_max_acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
|
||||
// An unset (0) limit means "no limit", as in the feedrate loop above. Only the Klipper
|
||||
// planner divides by the block acceleration, so leave the Marlin path as it was.
|
||||
if (m_flavor == gcfKlipper && axis_max_acceleration <= 0.0f)
|
||||
continue;
|
||||
if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration)
|
||||
acceleration = axis_max_acceleration / (std::abs(delta_pos[a]) * inv_distance);
|
||||
}
|
||||
|
||||
block.acceleration = acceleration;
|
||||
if (m_flavor == gcfKlipper) {
|
||||
block.junction_deviation = get_junction_deviation(static_cast<PrintEstimatedStatistics::ETimeMode>(i), acceleration);
|
||||
const float cruise_acceleration = machine.requested_accel_to_decel >= 0.0f ?
|
||||
std::min(machine.acceleration, machine.requested_accel_to_decel) :
|
||||
machine.acceleration * (1.0f - machine.minimum_cruise_ratio);
|
||||
block.mcr_delta_v2 = 2.0f * distance * std::min(acceleration, cruise_acceleration);
|
||||
}
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
@@ -5401,6 +5529,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
block.feedrate_profile.entry = std::min(vmax_junction, v_allowable);
|
||||
|
||||
block.max_entry_speed = vmax_junction;
|
||||
if (m_flavor == gcfKlipper && has_prev_move) {
|
||||
const TimeBlock& previous = blocks.back();
|
||||
block.max_entry_speed = std::min(block.max_entry_speed,
|
||||
std::sqrt(sqr(previous.max_entry_speed) + 2.0f * previous.distance * previous.acceleration));
|
||||
block.max_mcr_entry_speed_sqr = std::min(sqr(block.max_entry_speed), previous.max_mcr_entry_speed_sqr + previous.mcr_delta_v2);
|
||||
}
|
||||
block.flags.nominal_length = (block.feedrate_profile.cruise <= v_allowable);
|
||||
block.flags.recalculate = true;
|
||||
block.safe_feedrate = curr.safe_feedrate;
|
||||
@@ -5412,9 +5546,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
prev = curr;
|
||||
|
||||
blocks.push_back(block);
|
||||
if (m_flavor == gcfKlipper && blocks.size() > 1)
|
||||
machine.klipper_junction_flush -= distance / curr.feedrate;
|
||||
}
|
||||
|
||||
if (m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
|
||||
if (m_flavor == gcfKlipper ? m_time_processor.machines[0].klipper_junction_flush <= 0.0f :
|
||||
m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
|
||||
calculate_time(m_result, TimeProcessor::Planner::queue_size);
|
||||
|
||||
const Vec3f plate_offset = {(float) m_x_offset, (float) m_y_offset, 0.0f};
|
||||
@@ -6341,6 +6478,8 @@ void GCodeProcessor::process_M108(const GCodeReader::GCodeLine& line)
|
||||
|
||||
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
if (m_flavor == gcfKlipper)
|
||||
simulate_st_synchronize();
|
||||
int filament_id = get_filament_id();
|
||||
float new_temp;
|
||||
if (line.has_value('R', new_temp)) {
|
||||
@@ -6403,6 +6542,8 @@ void GCodeProcessor::process_M140(const GCodeReader::GCodeLine& line)
|
||||
|
||||
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
if (m_flavor == gcfKlipper)
|
||||
simulate_st_synchronize();
|
||||
float new_temp;
|
||||
if (line.has_value('S', new_temp))
|
||||
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
|
||||
@@ -6480,6 +6621,21 @@ void GCodeProcessor::process_M203(const GCodeReader::GCodeLine& line)
|
||||
void GCodeProcessor::process_M204(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
float value;
|
||||
if (m_flavor == gcfKlipper) {
|
||||
if (!line.has_value('S', value)) {
|
||||
float travel;
|
||||
if (!line.has_value('P', value) || !line.has_value('T', travel))
|
||||
return;
|
||||
value = std::min(value, travel);
|
||||
}
|
||||
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
|
||||
// setters would diverge from the firmware when custom G-code raises the limit.
|
||||
if (value > 0.0f) {
|
||||
for (auto& machine : m_time_processor.machines)
|
||||
machine.acceleration = machine.travel_acceleration = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal ||
|
||||
m_time_processor.machine_envelope_processing_enabled) {
|
||||
@@ -6541,50 +6697,50 @@ void GCodeProcessor::process_M205(const GCodeReader::GCodeLine& line)
|
||||
|
||||
void GCodeProcessor::process_SET_VELOCITY_LIMIT(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
// handle SQUARE_CORNER_VELOCITY
|
||||
static const std::regex square_corner_velocity_pattern("\\sSQUARE_CORNER_VELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
||||
std::smatch matches;
|
||||
if (std::regex_search(line.raw(), matches, square_corner_velocity_pattern) && matches.size() == 2) {
|
||||
float _jerk = 0;
|
||||
try
|
||||
{
|
||||
_jerk = std::stof(matches[1]);
|
||||
static const std::regex parameter_pattern(
|
||||
R"(\s(VELOCITY|ACCEL|ACCEL_TO_DECEL|SQUARE_CORNER_VELOCITY|MINIMUM_CRUISE_RATIO)\s*=\s*([+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)(?=\s|;|$))",
|
||||
std::regex::icase);
|
||||
const std::string command = line.raw().substr(0, line.raw().find(';'));
|
||||
std::vector<std::pair<std::string, float>> parameters;
|
||||
for (std::sregex_iterator it(command.begin(), command.end(), parameter_pattern), end; it != end; ++it) {
|
||||
float value;
|
||||
try {
|
||||
value = std::stof((*it)[2].str());
|
||||
} catch (const std::exception&) {
|
||||
continue;
|
||||
}
|
||||
catch (...){}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, _jerk);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, _jerk);
|
||||
if (std::isfinite(value))
|
||||
parameters.emplace_back(boost::to_upper_copy((*it)[1].str()), value);
|
||||
}
|
||||
// Klipper 0.13 converts ACCEL_TO_DECEL only when the line has no MINIMUM_CRUISE_RATIO, and later
|
||||
// releases ignore it, so a line carrying both (as Orca writes) is planned with the cruise ratio.
|
||||
const bool has_cruise_ratio = std::any_of(parameters.begin(), parameters.end(),
|
||||
[](const auto& parameter) { return parameter.first == "MINIMUM_CRUISE_RATIO" && parameter.second >= 0.0f && parameter.second < 1.0f; });
|
||||
for (const auto& [name, value] : parameters) {
|
||||
for (size_t i = 0; i < m_time_processor.machines.size(); ++i) {
|
||||
TimeMachine& machine = m_time_processor.machines[i];
|
||||
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
|
||||
// setters would diverge from the firmware when custom G-code raises the limit.
|
||||
if (name == "ACCEL" && value > 0.0f)
|
||||
machine.acceleration = machine.travel_acceleration = value;
|
||||
else if (name == "MINIMUM_CRUISE_RATIO" && value >= 0.0f && value < 1.0f) {
|
||||
if (machine.klipper_queue_priming && machine.requested_accel_to_decel >= 0.0f)
|
||||
machine.klipper_junction_flush -= 1.0f;
|
||||
machine.minimum_cruise_ratio = value;
|
||||
machine.requested_accel_to_decel = -1.0f;
|
||||
} else if (name == "ACCEL_TO_DECEL" && value > 0.0f && !has_cruise_ratio) {
|
||||
if (machine.klipper_queue_priming && machine.requested_accel_to_decel < 0.0f)
|
||||
machine.klipper_junction_flush += 1.0f;
|
||||
machine.requested_accel_to_decel = value;
|
||||
} else if (name == "SQUARE_CORNER_VELOCITY" && value >= 0.0f) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, value);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, value);
|
||||
} else if (name == "VELOCITY" && value > 0.0f) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, value);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const std::regex accel_pattern("\\sACCEL\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
||||
if (std::regex_search(line.raw(), matches, accel_pattern) && matches.size() == 2) {
|
||||
float _accl = 0;
|
||||
try
|
||||
{
|
||||
_accl = std::stof(matches[1]);
|
||||
}
|
||||
catch (...) {}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
set_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
|
||||
set_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
|
||||
}
|
||||
}
|
||||
|
||||
static const std::regex velocity_pattern("\\sVELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
||||
if (std::regex_search(line.raw(), matches, velocity_pattern) && matches.size() == 2) {
|
||||
float _speed = 0;
|
||||
try
|
||||
{
|
||||
_speed = std::stof(matches[1]);
|
||||
}
|
||||
catch (...) {}
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, _speed);
|
||||
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, _speed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_M221(const GCodeReader::GCodeLine& line)
|
||||
@@ -6621,14 +6777,15 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
|
||||
{
|
||||
float value_s = 0.0;
|
||||
float value_p = 0.0;
|
||||
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
|
||||
value_s += value_p * 0.001;
|
||||
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
||||
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
||||
if (m_skip_end_gcode_delays)
|
||||
return;
|
||||
simulate_st_synchronize(value_s);
|
||||
}
|
||||
const bool has_delay = line.has_value('S', value_s) || line.has_value('P', value_p);
|
||||
if (m_flavor != gcfKlipper && !has_delay)
|
||||
return;
|
||||
value_s += value_p * 0.001;
|
||||
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
||||
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
||||
if (m_skip_end_gcode_delays && has_delay)
|
||||
return;
|
||||
simulate_st_synchronize(value_s);
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line)
|
||||
@@ -7460,12 +7617,12 @@ float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode
|
||||
{
|
||||
const size_t id = static_cast<size_t>(mode);
|
||||
|
||||
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
|
||||
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
|
||||
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
|
||||
// Klipper derives junction deviation from the live toolhead acceleration,
|
||||
// before a move is constrained by Z or extruder limits.
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
|
||||
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
|
||||
acceleration = m_time_processor.machines[id].acceleration;
|
||||
if (scv <= 0.0f || acceleration <= 0.0f)
|
||||
return 0.0f;
|
||||
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
|
||||
@@ -7497,18 +7654,45 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
|
||||
const TimeMachine::State& curr, bool has_prev_move,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
if (m_flavor == gcfKlipper) {
|
||||
if (!has_prev_move || curr.enter_direction.isZero() || prev.exit_direction.isZero())
|
||||
return 0.0f;
|
||||
const TimeBlock& previous = m_time_processor.machines[static_cast<size_t>(mode)].blocks.back();
|
||||
double limit = std::min(sqr(double(block.feedrate_profile.cruise)), sqr(double(prev.feedrate)));
|
||||
const double extrude_ratio_change = std::abs(curr.axis_feedrate[E] / curr.feedrate -
|
||||
prev.axis_feedrate[E] / prev.feedrate);
|
||||
// Klipper's instantaneous_corner_velocity defaults to 1 mm/s.
|
||||
// It is independent of machine_max_jerk_e; custom firmware values
|
||||
// cannot be inferred from the slicer's jerk setting.
|
||||
if (extrude_ratio_change > 0.0f)
|
||||
limit = std::min(limit, sqr(1.0 / extrude_ratio_change));
|
||||
// Recover directions from the double-precision axis velocities: float
|
||||
// unit vectors lose the angle between nearly collinear short segments.
|
||||
const Vec3d prev_direction = Vec3d(prev.axis_feedrate[X], prev.axis_feedrate[Y], prev.axis_feedrate[Z]).normalized();
|
||||
const Vec3d curr_direction = Vec3d(curr.axis_feedrate[X], curr.axis_feedrate[Y], curr.axis_feedrate[Z]).normalized();
|
||||
const double cosine = std::clamp(-prev_direction.dot(curr_direction), -1.0, 1.0);
|
||||
const double sin_half = std::sqrt(0.5 * (1.0 - cosine));
|
||||
const double cos_half = std::sqrt(0.5 * (1.0 + cosine));
|
||||
if (sin_half < 1.0 && cos_half > 0.0) {
|
||||
const double radius = sin_half / (1.0 - sin_half);
|
||||
const double half_tan = 0.5 * sin_half / cos_half;
|
||||
limit = std::min({limit,
|
||||
radius * block.junction_deviation * block.acceleration,
|
||||
radius * previous.junction_deviation * previous.acceleration,
|
||||
half_tan * block.distance * block.acceleration,
|
||||
half_tan * previous.distance * previous.acceleration});
|
||||
}
|
||||
return std::sqrt(limit);
|
||||
}
|
||||
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
|
||||
if (junction_deviation <= 0.0f)
|
||||
return -1.0f; // classic jerk machine, the caller keeps its own computation
|
||||
if (!has_prev_move)
|
||||
return 0.0f; // starts from rest, the planner raises this on the reverse pass
|
||||
|
||||
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
|
||||
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
|
||||
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
|
||||
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
|
||||
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
|
||||
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
|
||||
// Unit vectors over XYZE keep this a cosine. Scaling by 1 / XYZ distance
|
||||
// leaves an E term that makes extruding corners appear straighter.
|
||||
// Marlin normalizes over XYZE; the Klipper branch above uses XYZ only.
|
||||
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
|
||||
if (junction_cos_theta > 0.999999f)
|
||||
return 0.0f; // the path doubles back, the machine has to stop
|
||||
@@ -7524,8 +7708,8 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
|
||||
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
|
||||
|
||||
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
|
||||
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
|
||||
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
||||
// capped by the centripetal acceleration it needs.
|
||||
if (block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
||||
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
|
||||
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
|
||||
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
|
||||
@@ -7700,6 +7884,7 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
|
||||
std::vector<TimeMachine::ActualSpeedMove> actual_speed_moves;
|
||||
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
||||
TimeMachine& machine = m_time_processor.machines[i];
|
||||
machine.klipper = m_flavor == gcfKlipper;
|
||||
machine.calculate_time(m_result, static_cast<PrintEstimatedStatistics::ETimeMode>(i), keep_last_n_blocks, additional_time, target_move_type, is_final);
|
||||
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal)
|
||||
actual_speed_moves = std::move(machine.actual_speed_moves);
|
||||
@@ -7778,6 +7963,14 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
|
||||
void GCodeProcessor::simulate_st_synchronize(float additional_time, EMoveType target_move_type)
|
||||
{
|
||||
calculate_time(m_result, 0, additional_time, target_move_type);
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// ToolHead overrides the normal look-ahead window with BUFFER_TIME_HIGH
|
||||
// at startup and after synchronization: 2 s on legacy, 1 s on modern Klipper.
|
||||
for (TimeMachine& machine : m_time_processor.machines) {
|
||||
machine.klipper_junction_flush = machine.requested_accel_to_decel >= 0.0f ? 2.0f : 1.0f;
|
||||
machine.klipper_queue_priming = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GCodeProcessor::update_estimated_times_stats()
|
||||
@@ -7812,7 +8005,7 @@ double GCodeProcessor::extract_absolute_position_on_axis(Axis axis, const GCodeR
|
||||
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
|
||||
|
||||
const double lengthsScaleFactor = (m_units == EUnits::Inches) ? double(INCHES_TO_MM) : 1.0;
|
||||
double ret = line.value(Slic3r::Axis(axis)) * lengthsScaleFactor;
|
||||
double ret = line.precise_value(Slic3r::Axis(axis)) * lengthsScaleFactor;
|
||||
// if (axis == E && m_use_volumetric_e)
|
||||
// ret /= area_filament_cross_section;
|
||||
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
|
||||
|
||||
@@ -91,12 +91,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
|
||||
struct Mode
|
||||
{
|
||||
float time;
|
||||
double time;
|
||||
float prepare_time;
|
||||
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> custom_gcode_times;
|
||||
|
||||
void reset() {
|
||||
time = 0.0f;
|
||||
time = 0.0;
|
||||
prepare_time = 0.0f;
|
||||
custom_gcode_times.clear();
|
||||
custom_gcode_times.shrink_to_fit();
|
||||
@@ -703,6 +703,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
float acceleration{ 0.0f }; // mm/s^2
|
||||
float max_entry_speed{ 0.0f }; // mm/s
|
||||
float safe_feedrate{ 0.0f }; // mm/s
|
||||
float junction_deviation{ 0.0f };
|
||||
float max_mcr_entry_speed_sqr{ 0.0f };
|
||||
float mcr_delta_v2{ 0.0f };
|
||||
// Peak cruise speed squared in effect when this block was planned. Only set on a block
|
||||
// that can accelerate; the delayed blocks after it are seeded from its value.
|
||||
float peak_cruise_v2{ 0.0f };
|
||||
Flags flags;
|
||||
FeedrateProfile feedrate_profile;
|
||||
Trapezoid trapezoid;
|
||||
@@ -782,6 +788,11 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
// hard limit for the travel acceleration, to which the firmware will clamp.
|
||||
float max_travel_acceleration; // mm/s^2
|
||||
float extrude_factor_override_percentage;
|
||||
bool klipper{ false };
|
||||
float minimum_cruise_ratio{ 0.5f };
|
||||
float requested_accel_to_decel{ -1.0f };
|
||||
float klipper_junction_flush{ 1.0f };
|
||||
bool klipper_queue_priming{ true };
|
||||
// We accumulate total print time in doubles to reduce the loss of precision
|
||||
// while adding big floating numbers with small float numbers.
|
||||
double time; // s
|
||||
@@ -818,6 +829,8 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
// Merge adjacent buffer entries that target the same move type.
|
||||
static AdditionalBuffer merge_adjacent_additional_time_blocks(const AdditionalBuffer& buffer);
|
||||
|
||||
size_t plan_klipper(bool lazy);
|
||||
|
||||
// additional_time is attributed to the first block matching target_move_type
|
||||
// (EMoveType::Noop matches any block, i.e. the first processed block).
|
||||
void calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks = 0, float additional_time = 0.0f, EMoveType target_move_type = EMoveType::Noop, bool is_final = false);
|
||||
@@ -1383,7 +1396,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
void process_buffer(const std::string& buffer);
|
||||
void finalize(bool post_process);
|
||||
|
||||
float get_time(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
double get_time(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
float get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const;
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#include "../ShortestPath.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -141,91 +141,54 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
|
||||
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
|
||||
};
|
||||
|
||||
// For many islands, the same scan with the edges binned in a uniform grid over their boxes, so each edge is only tested against the edges sharing a
|
||||
// cell with it - two edges whose boxes overlap always do. It returns the same crossing as the all-pairs scan
|
||||
// (smallest i, then smallest j), so the result is unchanged. The all-pairs scan is quadratic in the edge count and
|
||||
// runs again after every reversal; rebuilding the grid costs more than it saves below the threshold.
|
||||
constexpr size_t grid_min_size = 500;
|
||||
BoundingBox extent;
|
||||
for (size_t idx : path)
|
||||
extent.merge(centers[idx]);
|
||||
const int grid_n = std::clamp(int(std::sqrt(double(pn))), 1, 256);
|
||||
const coord_t cell_w = std::max<coord_t>(1, (extent.max.x() - extent.min.x()) / grid_n + 1);
|
||||
const coord_t cell_h = std::max<coord_t>(1, (extent.max.y() - extent.min.y()) / grid_n + 1);
|
||||
const auto for_cells = [&](const Point& a, const Point& b, auto&& fn) {
|
||||
const int x0 = int((std::min(a.x(), b.x()) - extent.min.x()) / cell_w), x1 = int((std::max(a.x(), b.x()) - extent.min.x()) / cell_w);
|
||||
const int y0 = int((std::min(a.y(), b.y()) - extent.min.y()) / cell_h), y1 = int((std::max(a.y(), b.y()) - extent.min.y()) / cell_h);
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
fn(y * grid_n + x);
|
||||
};
|
||||
std::vector<std::vector<size_t>> edge_cells(size_t(grid_n) * grid_n);
|
||||
|
||||
auto find_crossing_grid = [&]() -> std::pair<size_t, size_t> {
|
||||
for (std::vector<size_t>& cell : edge_cells)
|
||||
cell.clear();
|
||||
for (size_t j = 0; j < n_edges; ++j)
|
||||
for_cells(centers[path[j]], centers[path[(j + 1) % pn]], [&](int cell) { edge_cells[cell].emplace_back(j); });
|
||||
|
||||
for (size_t i = 0; i < n_edges; ++i) {
|
||||
const Point& ai = centers[path[i]];
|
||||
const Point& bi = centers[path[(i + 1) % pn]];
|
||||
|
||||
size_t first_j = std::numeric_limits<size_t>::max();
|
||||
for_cells(ai, bi, [&](int cell) {
|
||||
for (size_t j : edge_cells[cell]) {
|
||||
if (j < i + 2 || j >= first_j) continue;
|
||||
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
|
||||
if (i == 0 && j == pn - 1) continue;
|
||||
|
||||
const Point& aj = centers[path[j]];
|
||||
const Point& bj = centers[path[(j + 1) % pn]];
|
||||
|
||||
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
|
||||
if (Geometry::segments_intersect(ai, bi, aj, bj))
|
||||
first_j = j;
|
||||
}
|
||||
});
|
||||
if (first_j != std::numeric_limits<size_t>::max())
|
||||
return {i, first_j};
|
||||
}
|
||||
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
|
||||
};
|
||||
|
||||
// Process crossings one at a time: find first, reverse it, restart scan.
|
||||
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
|
||||
const int max_iters = static_cast<int>(pn * pn);
|
||||
int max_iters = static_cast<int>(pn * pn);
|
||||
bool improved = false;
|
||||
// Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on
|
||||
// islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same
|
||||
// orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps
|
||||
// to the ordering the capped loop would have stopped on are taken.
|
||||
std::unordered_map<uint64_t, int> seen_paths; // path hash -> reversals done when it was reached
|
||||
const auto path_hash = [&path]() {
|
||||
uint64_t h = 1469598103934665603ull; // FNV-1a
|
||||
for (size_t idx : path)
|
||||
h = (h ^ uint64_t(idx)) * 1099511628211ull;
|
||||
return h;
|
||||
};
|
||||
const auto reverse_first_crossing = [&]() {
|
||||
auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing();
|
||||
if (ci == std::numeric_limits<size_t>::max())
|
||||
return false;
|
||||
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
|
||||
return true;
|
||||
};
|
||||
seen_paths.emplace(path_hash(), 0);
|
||||
for (int iter = 1; iter <= max_iters && reverse_first_crossing(); ++iter) {
|
||||
while (max_iters-- > 0) {
|
||||
auto [ci, cj] = find_crossing();
|
||||
if (ci == std::numeric_limits<size_t>::max()) break;
|
||||
improved = true;
|
||||
if (auto [it, inserted] = seen_paths.emplace(path_hash(), iter); !inserted) {
|
||||
for (int steps = (max_iters - iter) % (iter - it->second); steps > 0; --steps)
|
||||
reverse_first_crossing();
|
||||
break;
|
||||
}
|
||||
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
|
||||
}
|
||||
return improved;
|
||||
}
|
||||
|
||||
std::vector<size_t> tsp_exact_cycle(const Points& centers)
|
||||
{
|
||||
const size_t n = centers.size();
|
||||
std::vector<size_t> path(n);
|
||||
std::iota(path.begin(), path.end(), 0);
|
||||
if (n <= 3) return path;
|
||||
// State mask * m + j: the points 1..m of mask visited after point 0, the last one j + 1.
|
||||
const size_t m = n - 1;
|
||||
assert(m <= 16);
|
||||
std::vector<double> cost((size_t(1) << m) * m, std::numeric_limits<double>::max());
|
||||
std::vector<size_t> from(cost.size(), size_t(-1));
|
||||
auto dist = [¢ers](size_t a, size_t b) { return (centers[a].cast<double>() - centers[b].cast<double>()).norm(); };
|
||||
for (size_t j = 0; j < m; ++j)
|
||||
cost[(size_t(1) << j) * m + j] = dist(0, j + 1);
|
||||
for (size_t mask = 1; mask < (size_t(1) << m); ++mask)
|
||||
for (size_t j = 0; j < m; ++j)
|
||||
if (const double c = cost[mask * m + j]; c < std::numeric_limits<double>::max())
|
||||
for (size_t k = 0; k < m; ++k)
|
||||
if (!(mask >> k & 1)) {
|
||||
const size_t next = (mask | size_t(1) << k) * m + k;
|
||||
if (const double nc = c + dist(j + 1, k + 1); nc < cost[next]) {
|
||||
cost[next] = nc;
|
||||
from[next] = mask * m + j;
|
||||
}
|
||||
}
|
||||
const size_t full = (size_t(1) << m) - 1;
|
||||
size_t best = full * m;
|
||||
for (size_t j = 1; j < m; ++j)
|
||||
if (cost[full * m + j] + dist(j + 1, 0) < cost[best] + dist(best % m + 1, 0))
|
||||
best = full * m + j;
|
||||
for (size_t s = best, k = n; k-- > 1; s = from[s])
|
||||
path[k] = s % m + 1;
|
||||
return path;
|
||||
}
|
||||
|
||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
size_t pn = path.size();
|
||||
|
||||
@@ -29,6 +29,9 @@ bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_
|
||||
// Returns true if any crossing was removed.
|
||||
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
|
||||
|
||||
// The shortest cycle through the points, starting at point 0, by dynamic programming over subsets; for a few points only.
|
||||
std::vector<size_t> tsp_exact_cycle(const Points& centers);
|
||||
|
||||
// Rotate the cycle so the closing edge (last -> first) is minimized.
|
||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -1208,21 +1207,21 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|
||||
const size_t layer_idx, const float max_distance,
|
||||
const SeamPlacerImpl::SeamComparator &comparator) const {
|
||||
using namespace SeamPlacerImpl;
|
||||
// Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within
|
||||
// the radius, so they are looked at as the search finds them rather than collected into a vector first.
|
||||
constexpr size_t none = std::numeric_limits<size_t>::max();
|
||||
size_t best_nearby_point_index = none;
|
||||
size_t nearest_point_index = none;
|
||||
visit_nearby_points(*layers[layer_idx].points_tree, projected_position, max_distance,
|
||||
[&layers, &comparator, &projected_position, layer_idx, &best_nearby_point_index, &nearest_point_index]
|
||||
(size_t nearby_point_index) {
|
||||
if (best_nearby_point_index == none) {
|
||||
// The first point found starts both, as the first of the collected ones did.
|
||||
best_nearby_point_index = nearest_point_index = nearby_point_index;
|
||||
}
|
||||
std::vector<size_t> nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position,
|
||||
max_distance);
|
||||
|
||||
if (nearby_points_indices.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
size_t best_nearby_point_index = nearby_points_indices[0];
|
||||
size_t nearest_point_index = nearby_points_indices[0];
|
||||
|
||||
// Now find best nearby point, nearest point, and corresponding indices
|
||||
for (const size_t &nearby_point_index : nearby_points_indices) {
|
||||
const SeamCandidate &point = layers[layer_idx].points[nearby_point_index];
|
||||
if (point.perimeter.finalized) {
|
||||
return; // skip over finalized perimeters, try to find some that is not finalized
|
||||
continue; // skip over finalized perimeters, try to find some that is not finalized
|
||||
}
|
||||
if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index],
|
||||
projected_position.head<2>())
|
||||
@@ -1234,10 +1233,6 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|
||||
|| layers[layer_idx].points[nearest_point_index].perimeter.finalized) {
|
||||
nearest_point_index = nearby_point_index;
|
||||
}
|
||||
});
|
||||
|
||||
if (best_nearby_point_index == none) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index];
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "libslic3r/GCodeWriter.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
@@ -1528,8 +1529,10 @@ private:
|
||||
gcode << "M204 P" << acceleration;
|
||||
}
|
||||
else if (m_gcode_flavor == gcfKlipper && m_accel_to_decel_enable) {
|
||||
gcode << "SET_VELOCITY_LIMIT ACCEL_TO_DECEL=" << acceleration * m_accel_to_decel_factor / 100;
|
||||
gcode << "\nM204 S" << acceleration;
|
||||
// One line, so Klipper derives the limit from this acceleration rather than the previous one.
|
||||
std::string limits = "SET_VELOCITY_LIMIT ACCEL=" + std::to_string(acceleration);
|
||||
GCodeWriter::append_klipper_accel_to_decel(limits, acceleration, m_accel_to_decel_factor);
|
||||
gcode << limits;
|
||||
}
|
||||
else {
|
||||
// M204: Set default acceleration
|
||||
|
||||
@@ -95,7 +95,7 @@ const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, G
|
||||
if (pend != c && is_end_of_word(*pend)) {
|
||||
// The axis value has been parsed correctly.
|
||||
if (axis != UNKNOWN_AXIS)
|
||||
gline.m_axis[int(axis)] = float(v);
|
||||
gline.m_axis[int(axis)] = v;
|
||||
gline.m_mask |= 1 << int(axis);
|
||||
c = pend;
|
||||
} else
|
||||
|
||||
@@ -35,7 +35,8 @@ public:
|
||||
std::string_view axis_pos(char axis) const;
|
||||
void clear() { m_raw.clear(); }
|
||||
bool has(Axis axis) const { return (m_mask & (1 << int(axis))) != 0; }
|
||||
float value(Axis axis) const { return m_axis[axis]; }
|
||||
float value(Axis axis) const { return float(m_axis[axis]); }
|
||||
double precise_value(Axis axis) const { return m_axis[axis]; }
|
||||
bool has(char axis) const;
|
||||
bool has_value(char axis, float &value) const;
|
||||
// Parse value of an axis from raw string starting at axis_pos.
|
||||
@@ -72,15 +73,15 @@ public:
|
||||
bool has_p() const { return this->has(P); }
|
||||
|
||||
bool has_unknown_axis() const { return this->has(UNKNOWN_AXIS); }
|
||||
float x() const { return m_axis[X]; }
|
||||
float y() const { return m_axis[Y]; }
|
||||
float z() const { return m_axis[Z]; }
|
||||
float e() const { return m_axis[E]; }
|
||||
float f() const { return m_axis[F]; }
|
||||
float x() const { return float(m_axis[X]); }
|
||||
float y() const { return float(m_axis[Y]); }
|
||||
float z() const { return float(m_axis[Z]); }
|
||||
float e() const { return float(m_axis[E]); }
|
||||
float f() const { return float(m_axis[F]); }
|
||||
// BBS: add I J P axis
|
||||
float i() const { return m_axis[I]; }
|
||||
float j() const { return m_axis[J]; }
|
||||
float p() const { return m_axis[P]; }
|
||||
float i() const { return float(m_axis[I]); }
|
||||
float j() const { return float(m_axis[J]); }
|
||||
float p() const { return float(m_axis[P]); }
|
||||
|
||||
static bool cmd_is(const std::string &gcode_line, const char *cmd_test) {
|
||||
const char *cmd = GCodeReader::skip_whitespaces(gcode_line.c_str());
|
||||
@@ -100,7 +101,7 @@ public:
|
||||
}
|
||||
private:
|
||||
std::string m_raw;
|
||||
float m_axis[NUM_AXES];
|
||||
double m_axis[NUM_AXES];
|
||||
uint32_t m_mask;
|
||||
friend class GCodeReader;
|
||||
};
|
||||
|
||||
@@ -453,12 +453,11 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
|
||||
gcode += separate_travel ? "M204 T" : "M204 P";
|
||||
gcode += value;
|
||||
} else if (FLAVOR_IS(gcfKlipper)) {
|
||||
gcode.reserve(96);
|
||||
gcode.reserve(128);
|
||||
gcode += "SET_VELOCITY_LIMIT ACCEL=";
|
||||
gcode += value;
|
||||
if (this->config.accel_to_decel_enable) {
|
||||
gcode += " ACCEL_TO_DECEL=";
|
||||
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
|
||||
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
|
||||
if (GCodeWriter::full_gcode_comment)
|
||||
gcode += " ; adjust ACCEL_TO_DECEL";
|
||||
}
|
||||
@@ -473,6 +472,14 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
|
||||
return gcode;
|
||||
}
|
||||
|
||||
void GCodeWriter::append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor)
|
||||
{
|
||||
gcode += " ACCEL_TO_DECEL=";
|
||||
gcode += float_to_string_decimal_point(acceleration * accel_to_decel_factor / 100);
|
||||
gcode += " MINIMUM_CRUISE_RATIO=";
|
||||
gcode += float_to_string_decimal_point(1. - accel_to_decel_factor / 100);
|
||||
}
|
||||
|
||||
std::string GCodeWriter::set_jerk_xy(double jerk)
|
||||
{
|
||||
if (jerk < 0.01 || is_approx(jerk, m_last_jerk))
|
||||
@@ -548,15 +555,13 @@ std::string GCodeWriter::set_accel_and_jerk(unsigned int acceleration, double je
|
||||
return std::string();
|
||||
|
||||
std::string gcode;
|
||||
gcode.reserve(96);
|
||||
gcode.reserve(128);
|
||||
gcode += "SET_VELOCITY_LIMIT";
|
||||
if (set_acceleration) {
|
||||
gcode += " ACCEL=";
|
||||
gcode += std::to_string(acceleration);
|
||||
if (this->config.accel_to_decel_enable) {
|
||||
gcode += " ACCEL_TO_DECEL=";
|
||||
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
|
||||
}
|
||||
if (this->config.accel_to_decel_enable)
|
||||
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
|
||||
m_last_acceleration = acceleration;
|
||||
}
|
||||
if (set_jerk) {
|
||||
|
||||
@@ -71,6 +71,10 @@ public:
|
||||
std::string set_jerk_xy(double jerk);
|
||||
// Orca: set acceleration and jerk in one command for Klipper
|
||||
std::string set_accel_and_jerk(unsigned int acceleration, double jerk);
|
||||
// Orca: Klipper's accel_to_decel limit for `acceleration`, sent as the legacy ACCEL_TO_DECEL and as the
|
||||
// equivalent MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL that replaced it. Klipper before 0.13 and
|
||||
// vendor forks read only the first, current Klipper only the second, and 0.13 prefers the second.
|
||||
static void append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor);
|
||||
std::string set_junction_deviation(double junction_deviation);
|
||||
std::string set_pressure_advance(double pa, int tool = -1) const;
|
||||
std::string set_input_shaping(char axis, float damp, float freq, std::string type) const;
|
||||
|
||||
@@ -318,36 +318,6 @@ std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const P
|
||||
return visitor.result;
|
||||
}
|
||||
|
||||
// Visits the points within max_distance of center, in the order find_nearby_points() would collect them, and hands
|
||||
// each of them to `visitor_fn` instead of returning them all: a search over a dense set spends more on collecting the
|
||||
// points into a vector than on the search itself, and its caller usually keeps only a few of them.
|
||||
template<typename KDTreeIndirectType, typename PointType, typename VisitorFn>
|
||||
void visit_nearby_points(const KDTreeIndirectType &kdtree, const PointType ¢er,
|
||||
const typename KDTreeIndirectType::CoordType &max_distance, VisitorFn visitor_fn)
|
||||
{
|
||||
using CoordType = typename KDTreeIndirectType::CoordType;
|
||||
|
||||
struct Visitor {
|
||||
const KDTreeIndirectType &kdtree;
|
||||
const PointType center;
|
||||
const CoordType max_distance_squared;
|
||||
VisitorFn visitor_fn;
|
||||
|
||||
unsigned int operator()(size_t idx, size_t dimension) {
|
||||
auto dist = CoordType(0);
|
||||
for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) {
|
||||
CoordType d = center[i] - kdtree.coordinate(idx, i);
|
||||
dist += d * d;
|
||||
}
|
||||
if (dist < max_distance_squared)
|
||||
visitor_fn(idx);
|
||||
return kdtree.descent_mask(center[dimension], max_distance_squared, idx, dimension);
|
||||
}
|
||||
} visitor { kdtree, center, max_distance * max_distance, visitor_fn };
|
||||
|
||||
kdtree.visit(visitor);
|
||||
}
|
||||
|
||||
template<typename KDTreeIndirectType, typename PointType>
|
||||
std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType ¢er,
|
||||
const typename KDTreeIndirectType::CoordType& max_distance)
|
||||
|
||||
@@ -99,11 +99,10 @@ void LayerRegion::slices_to_fill_surfaces_clipped()
|
||||
by_surface[size_t(surface.surface_type)].emplace_back(&surface);
|
||||
// Trim surfaces by the fill_boundaries.
|
||||
this->fill_surfaces.surfaces.clear();
|
||||
const Polygons fill_boundaries = to_polygons(this->fill_expolygons);
|
||||
for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
|
||||
const SurfacesPtr &this_surfaces = by_surface[surface_type];
|
||||
if (! this_surfaces.empty())
|
||||
this->fill_surfaces.append(intersection_ex_by_piece(to_expolygons(this_surfaces), fill_boundaries), SurfaceType(surface_type));
|
||||
this->fill_surfaces.append(intersection_ex(this_surfaces, this->fill_expolygons), SurfaceType(surface_type));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "Surface.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <numeric>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <list>
|
||||
@@ -1362,15 +1361,10 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
}
|
||||
#endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM
|
||||
|
||||
// When the upper surface of an object is occluded, it should no longer be considered the upper surface.
|
||||
// Every (colour, layer) pair is trimmed on its own, so they all run at once: the painted faces of a finely
|
||||
// textured part project hundreds of thousands of triangles onto one layer, which used to be trimmed serially.
|
||||
// When the upper surface of an object is occluded, it should no longer be considered the upper surface
|
||||
{
|
||||
const size_t occluded_pairs = num_facets_states * layers.size();
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, occluded_pairs), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t pair_idx = range.begin(); pair_idx < range.end(); ++pair_idx) {
|
||||
const size_t extruder_idx = pair_idx / layers.size();
|
||||
const size_t layer_idx = pair_idx % layers.size();
|
||||
for (size_t extruder_idx = 0; extruder_idx < num_facets_states; ++extruder_idx) {
|
||||
for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) {
|
||||
if (!top_raw[extruder_idx].empty() && !top_raw[extruder_idx][layer_idx].empty() && layer_idx + 1 < layers.size()) {
|
||||
top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]);
|
||||
}
|
||||
@@ -1378,7 +1372,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states);
|
||||
@@ -1436,58 +1430,11 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
return out;
|
||||
};
|
||||
|
||||
// Projects a painted top or bottom face `ex` of layer `layer_idx` onto the shell layers below or above it (in
|
||||
// `shell_layers`, nearest first), one more perimeter in on each, stopping at the first layer where nothing is left.
|
||||
// Only the slices within the deepest offset of `ex` (three times that with the miter joins) decide the result, so the
|
||||
// work is done per tile of `ex`'s ExPolygons on the slices cut to the tile's box grown by that much: the same result, but
|
||||
// each Clipper call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in
|
||||
// parallel.
|
||||
const auto project_to_shells = [&input_expolygons](const ExPolygons &ex, size_t layer_idx, const std::vector<size_t> &shell_layers,
|
||||
const LayerColorStat &stat, ShellProjections &dst) {
|
||||
std::vector<float> offsets(shell_layers.size());
|
||||
float offset = 0.f;
|
||||
for (size_t i = 0; i < shell_layers.size(); ++i) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
offsets[i] = offset;
|
||||
}
|
||||
if (offsets.empty())
|
||||
return;
|
||||
const coord_t reach = coord_t(std::ceil(DefaultMiterLimit * std::abs(offsets.back()))) + 10 * SCALED_EPSILON;
|
||||
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(ex, 16);
|
||||
// [shell layer][tile]
|
||||
std::vector<std::vector<ExPolygons>> shells(shell_layers.size(), std::vector<ExPolygons>(tiles.size()));
|
||||
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
|
||||
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
|
||||
const BoundingBox bbox = tile.bbox.inflated(reach);
|
||||
ExPolygons tile_ex;
|
||||
tile_ex.reserve(tile.members.size());
|
||||
for (size_t i : tile.members)
|
||||
tile_ex.emplace_back(ex[i]);
|
||||
Polygons layer_slices_trimmed = ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[layer_idx], bbox);
|
||||
for (size_t i = 0; i < shell_layers.size() && ! layer_slices_trimmed.empty(); ++i) {
|
||||
const ExPolygons trimmed = intersection_ex(layer_slices_trimmed, ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[shell_layers[i]], bbox));
|
||||
shells[i][tile_idx] = opening_ex(intersection_ex(tile_ex, offset_ex(trimmed, offsets[i])), stat.small_region_threshold);
|
||||
layer_slices_trimmed = to_polygons(trimmed);
|
||||
}
|
||||
});
|
||||
for (size_t i = 0; i < shell_layers.size(); ++i) {
|
||||
ExPolygons shell;
|
||||
for (ExPolygons &tile_shell : shells[i])
|
||||
append(shell, std::move(tile_shell));
|
||||
if (shell.empty())
|
||||
break;
|
||||
dst.emplace_back(shell_layers[i], std::move(shell));
|
||||
}
|
||||
};
|
||||
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
|
||||
&throw_on_cancel_callback, &bottom_raw, &triangles_by_color_bottom, &project_to_shells,
|
||||
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
|
||||
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
|
||||
// Each colour writes only its own vectors, so the colours run in parallel: a painted top or bottom face
|
||||
// projects onto a single layer, which otherwise did all of its colours on one thread.
|
||||
tbb::parallel_for(size_t(0), size_t(num_facets_states), [&](size_t color_idx) {
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
|
||||
throw_on_cancel_callback();
|
||||
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
|
||||
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
|
||||
@@ -1496,10 +1443,18 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
top_ex = opening_ex(top_ex, stat.small_region_threshold);
|
||||
if (! top_ex.empty()) {
|
||||
append(triangles_by_color_top[color_idx][layer_idx], top_ex);
|
||||
std::vector<size_t> shell_layers;
|
||||
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx)
|
||||
shell_layers.emplace_back(size_t(last_idx));
|
||||
project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width ;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
|
||||
@@ -1508,13 +1463,21 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
|
||||
if (! bottom_ex.empty()) {
|
||||
append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex);
|
||||
std::vector<size_t> shell_layers;
|
||||
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx)
|
||||
shell_layers.emplace_back(last_idx);
|
||||
project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]);
|
||||
float offset = 0.f;
|
||||
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
|
||||
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
|
||||
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
|
||||
//offset -= stat.extrusion_width;
|
||||
offset -= (stat.extrusion_spacing + stat.extrusion_width);
|
||||
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
|
||||
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
|
||||
if (last.empty())
|
||||
break;
|
||||
shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1535,23 +1498,20 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
&shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
|
||||
throw_on_cancel_callback();
|
||||
// The per-colour unions below are independent of each other, so they run in parallel (a painted top or
|
||||
// bottom face puts all of its colours on one layer); whatever combines the colours stays in colour order.
|
||||
const auto merge_colour_union = [&](size_t color_idx) {
|
||||
ExPolygons painted_exploys;
|
||||
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
|
||||
auto &self = triangles_by_color_merged[color_idx][layer_idx];
|
||||
append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx]));
|
||||
append(self, std::move(triangles_by_color_top[color_idx][layer_idx]));
|
||||
self = union_ex(self);
|
||||
};
|
||||
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), merge_colour_union);
|
||||
|
||||
ExPolygons painted_exploys;
|
||||
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx)
|
||||
append(painted_exploys, triangles_by_color_merged[color_idx][layer_idx]);
|
||||
append(painted_exploys, self);
|
||||
}
|
||||
|
||||
painted_exploys = union_ex(painted_exploys);
|
||||
|
||||
//BBS: merge the top and bottom shell layers
|
||||
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), [&](size_t color_idx) {
|
||||
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
|
||||
auto &self = triangles_by_color_merged[color_idx][layer_idx];
|
||||
|
||||
auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys);
|
||||
@@ -1560,7 +1520,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
append(self, top_area);
|
||||
append(self, bottom_area);
|
||||
self = union_ex(self);
|
||||
});
|
||||
}
|
||||
// Trim one region by the other if some of the regions overlap.
|
||||
ExPolygons painted_regions;
|
||||
for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) {
|
||||
@@ -1927,69 +1887,7 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Finds the islands (layer ExPolygons) a region piece overlaps. A top or bottom region is projected from the neighbouring
|
||||
// layers and may reach past the island it belongs to, or over several islands.
|
||||
class IslandLocator
|
||||
{
|
||||
public:
|
||||
explicit IslandLocator(const ExPolygons &islands) : m_islands(islands)
|
||||
{
|
||||
m_bboxes.reserve(islands.size());
|
||||
for (const ExPolygon &island : islands) {
|
||||
m_bboxes.emplace_back(get_extents(island));
|
||||
m_extent.merge(m_bboxes.back());
|
||||
}
|
||||
if (!m_extent.defined)
|
||||
return;
|
||||
const Point size = m_extent.size();
|
||||
m_cell_w = std::max<coord_t>(1, size.x() / GRID + 1);
|
||||
m_cell_h = std::max<coord_t>(1, size.y() / GRID + 1);
|
||||
m_grid.assign(GRID * GRID, {});
|
||||
for (size_t i = 0; i < m_bboxes.size(); ++i)
|
||||
for_cells(m_bboxes[i], [&](int cell) { m_grid[cell].emplace_back(i); });
|
||||
}
|
||||
|
||||
void find(const ExPolygon &piece, std::vector<size_t> &out) const
|
||||
{
|
||||
out.clear();
|
||||
const BoundingBox bbox = get_extents(piece);
|
||||
if (!m_extent.defined || !m_extent.overlap(bbox))
|
||||
return;
|
||||
for_cells(bbox, [&](int cell) {
|
||||
for (size_t i : m_grid[cell])
|
||||
if (m_bboxes[i].overlap(bbox))
|
||||
out.emplace_back(i);
|
||||
});
|
||||
sort_remove_duplicates(out);
|
||||
if (out.size() > 1)
|
||||
out.erase(std::remove_if(out.begin(), out.end(), [&](size_t i) {
|
||||
const BoundingBox common(m_bboxes[i].min.cwiseMax(bbox.min), m_bboxes[i].max.cwiseMin(bbox.max));
|
||||
return intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(piece, common.inflated(SCALED_EPSILON)),
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(m_islands[i], common.inflated(SCALED_EPSILON))).empty();
|
||||
}), out.end());
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr int GRID = 64;
|
||||
template<typename Fn> void for_cells(const BoundingBox &bb, Fn &&fn) const
|
||||
{
|
||||
const int x0 = std::clamp(int((bb.min.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1), x1 = std::clamp(int((bb.max.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1);
|
||||
const int y0 = std::clamp(int((bb.min.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1), y1 = std::clamp(int((bb.max.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1);
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
fn(y * GRID + x);
|
||||
}
|
||||
|
||||
const ExPolygons &m_islands;
|
||||
std::vector<BoundingBox> m_bboxes;
|
||||
BoundingBox m_extent;
|
||||
coord_t m_cell_w = 1, m_cell_h = 1;
|
||||
std::vector<std::vector<size_t>> m_grid;
|
||||
};
|
||||
|
||||
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<ExPolygons> &input_expolygons,
|
||||
const std::vector<std::vector<ExPolygons>> &segmented_regions,
|
||||
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<std::vector<ExPolygons>> &segmented_regions,
|
||||
std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers,
|
||||
const size_t num_facets_states,
|
||||
const std::function<void()> &throw_on_cancel_callback)
|
||||
@@ -2000,91 +1898,33 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
|
||||
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
|
||||
// Every region of a layer is merged together with the regions of the islands it overlaps, and the islands are further
|
||||
// apart than the dimple removal below reaches, so this gives the same result as merging the layer at once. On a layer
|
||||
// cut through a fine relief every region shares thousands of hole contours with every other, and Clipper, splitting
|
||||
// and re-linking one huge polygon over and over, took anything up to half an hour for a layer; per island each operation
|
||||
// stays the size of the island, and the islands run in parallel.
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&](const tbb::blocked_range<size_t> &range) {
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
|
||||
assert(segmented_regions[layer_idx].size() == num_facets_states);
|
||||
throw_on_cancel_callback();
|
||||
// Group the islands joined by a region overlapping several of them; the last group takes the regions lying
|
||||
// outside every island.
|
||||
const ExPolygons &islands = input_expolygons[layer_idx];
|
||||
const IslandLocator locator(islands);
|
||||
std::vector<size_t> parent(islands.size() + 1);
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
const auto root = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
// Islands of every piece: side regions of colours 1.., then top/bottom regions of colours 0..
|
||||
std::vector<const ExPolygon *> pieces;
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
|
||||
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
|
||||
pieces.emplace_back(&piece);
|
||||
if (!top_and_bottom_layers.empty())
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
|
||||
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
|
||||
pieces.emplace_back(&piece);
|
||||
std::vector<std::vector<size_t>> overlapped(pieces.size());
|
||||
tbb::parallel_for(size_t(0), pieces.size(), [&](size_t i) { locator.find(*pieces[i], overlapped[i]); });
|
||||
std::vector<size_t> piece_island(pieces.size());
|
||||
for (size_t i = 0; i < pieces.size(); ++i) {
|
||||
piece_island[i] = overlapped[i].empty() ? islands.size() : overlapped[i].front();
|
||||
for (size_t island : overlapped[i])
|
||||
parent[root(island)] = root(piece_island[i]);
|
||||
}
|
||||
std::vector<size_t> bucket_of(parent.size(), size_t(-1));
|
||||
size_t num_buckets = 0;
|
||||
for (size_t i = 0; i < parent.size(); ++i)
|
||||
if (size_t &b = bucket_of[root(i)]; b == size_t(-1))
|
||||
b = num_buckets++;
|
||||
|
||||
// [bucket][colour]
|
||||
std::vector<std::vector<ExPolygons>> sides(num_buckets, std::vector<ExPolygons>(num_facets_states));
|
||||
std::vector<std::vector<ExPolygons>> tops(num_buckets, std::vector<ExPolygons>(num_facets_states));
|
||||
size_t piece_idx = 0;
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
|
||||
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
|
||||
sides[bucket_of[root(piece_island[piece_idx++])]][extruder_id].emplace_back(piece);
|
||||
if (!top_and_bottom_layers.empty())
|
||||
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
|
||||
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
|
||||
tops[bucket_of[root(piece_island[piece_idx++])]][color_idx].emplace_back(piece);
|
||||
|
||||
// Side regions minus the top/bottom regions of every colour.
|
||||
std::vector<std::vector<ExPolygons>> merged(num_buckets, std::vector<ExPolygons>(num_facets_states));
|
||||
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
|
||||
Polygons tops_all;
|
||||
for (const ExPolygons &t : tops[bucket])
|
||||
polygons_append(tops_all, t);
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
|
||||
if (!sides[bucket][extruder_id].empty())
|
||||
merged[bucket][extruder_id] = tops_all.empty() ? std::move(sides[bucket][extruder_id]) :
|
||||
diff_ex_by_piece(sides[bucket][extruder_id], tops_all);
|
||||
});
|
||||
|
||||
// Then this colour's top/bottom regions, with the dimples removed (#7235) when the layer has side regions left.
|
||||
// Zero is skipped because it is the default color of the volume
|
||||
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
|
||||
if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) {
|
||||
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
|
||||
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
|
||||
continue;
|
||||
throw_on_cancel_callback();
|
||||
if (!segmented_regions[layer_idx][extruder_id].empty()) {
|
||||
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id];
|
||||
if (!top_and_bottom_layers.empty()) {
|
||||
for (const std::vector<ExPolygons> &top_and_bottom_by_extruder : top_and_bottom_layers) {
|
||||
if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) {
|
||||
segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
|
||||
}
|
||||
|
||||
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
|
||||
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
|
||||
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
|
||||
|
||||
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
|
||||
if (!was_top_and_bottom_empty)
|
||||
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
|
||||
}
|
||||
bool was_top_and_bottom_empty = true;
|
||||
for (size_t bucket = 0; bucket < num_buckets && was_top_and_bottom_empty; ++bucket)
|
||||
was_top_and_bottom_empty = merged[bucket][extruder_id].empty();
|
||||
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
|
||||
ExPolygons ®ion = merged[bucket][extruder_id];
|
||||
append(region, tops[bucket][extruder_id]);
|
||||
if (!was_top_and_bottom_empty && !region.empty())
|
||||
region = offset2_ex(union_ex(region), float(SCALED_EPSILON), -float(SCALED_EPSILON));
|
||||
});
|
||||
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
|
||||
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
|
||||
}
|
||||
}
|
||||
}); // end of parallel_for
|
||||
@@ -2372,56 +2212,16 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
|
||||
|
||||
assert(!color_poly.empty());
|
||||
assert(!color_poly.front().empty());
|
||||
// Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to
|
||||
// that island's contours than to any other island's - the way out crosses its own boundary first - so its
|
||||
// Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief
|
||||
// has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions
|
||||
// that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in
|
||||
// parallel; an island in a single colour needs no diagram at all.
|
||||
const ExPolygons &islands = input_expolygons[layer_idx];
|
||||
std::vector<std::pair<size_t, size_t>> island_contours(islands.size()); // [first, last) into color_poly
|
||||
{
|
||||
// The same order EdgeGrid::Grid::create() lists the contours in, and so colorize_contours().
|
||||
size_t idx = 0;
|
||||
for (size_t island_idx = 0; island_idx < islands.size(); ++island_idx) {
|
||||
const size_t first = idx;
|
||||
if (!islands[island_idx].contour.empty())
|
||||
++idx;
|
||||
for (const Polygon &hole : islands[island_idx].holes)
|
||||
if (!hole.empty())
|
||||
++idx;
|
||||
island_contours[island_idx] = {first, idx};
|
||||
}
|
||||
assert(idx == color_poly.size());
|
||||
if (has_layer_only_one_color(color_poly)) {
|
||||
// If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer.
|
||||
segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx];
|
||||
} else {
|
||||
MMU_Graph graph = build_graph(layer_idx, color_poly);
|
||||
remove_multiple_edges_in_vertices(graph, color_poly);
|
||||
graph.remove_nodes_with_one_arc();
|
||||
segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states);
|
||||
//segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx);
|
||||
}
|
||||
std::vector<std::vector<ExPolygons>> island_regions(islands.size());
|
||||
tbb::parallel_for(size_t(0), islands.size(), [&](size_t island_idx) {
|
||||
const auto [first, last] = island_contours[island_idx];
|
||||
if (first == last)
|
||||
return;
|
||||
const std::vector<ColoredLines> island_poly(color_poly.begin() + first, color_poly.begin() + last);
|
||||
std::vector<ExPolygons> ®ions = island_regions[island_idx];
|
||||
if (has_layer_only_one_color(island_poly)) {
|
||||
regions.assign(num_facets_states, ExPolygons());
|
||||
regions[size_t(island_poly.front().front().color)].emplace_back(islands[island_idx]);
|
||||
} else {
|
||||
MMU_Graph graph = build_graph(layer_idx, island_poly);
|
||||
remove_multiple_edges_in_vertices(graph, island_poly);
|
||||
graph.remove_nodes_with_one_arc();
|
||||
regions = extract_colored_segments(graph, num_facets_states);
|
||||
// The faces of one colour tile it without overlapping; merged here, where an island is small,
|
||||
// every later boolean gets a few regions instead of thousands of faces sharing their edges. An
|
||||
// island with many holes keeps its faces: merged, each colour would be one region with thousands
|
||||
// of holes, and subtracting from that is far slower than from the faces one at a time.
|
||||
if (island_poly.size() <= 64)
|
||||
for (ExPolygons &faces : regions)
|
||||
if (faces.size() > 1)
|
||||
faces = union_ex(faces);
|
||||
}
|
||||
});
|
||||
for (std::vector<ExPolygons> ®ions : island_regions)
|
||||
for (size_t color_idx = 0; color_idx < regions.size(); ++color_idx)
|
||||
append(segmented_regions[layer_idx][color_idx], std::move(regions[color_idx]));
|
||||
|
||||
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
|
||||
export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]);
|
||||
@@ -2444,7 +2244,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
|
||||
throw_on_cancel_callback();
|
||||
}
|
||||
|
||||
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(input_expolygons, segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
|
||||
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
|
||||
throw_on_cancel_callback();
|
||||
|
||||
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
|
||||
|
||||
@@ -23,15 +23,11 @@ public:
|
||||
|
||||
MultiPoint() {}
|
||||
MultiPoint(const MultiPoint &other) : points(other.points) {}
|
||||
MultiPoint(MultiPoint &&other) noexcept : points(std::move(other.points)) {}
|
||||
MultiPoint(MultiPoint &&other) : points(std::move(other.points)) {}
|
||||
MultiPoint(std::initializer_list<Point> list) : points(list) {}
|
||||
explicit MultiPoint(const Points &_points) : points(_points) {}
|
||||
// Without it, the derived classes' move constructors passing std::move(points) here copied them, which
|
||||
// also means a moved-from Polygon or Polyline is now really empty where it used to silently keep its
|
||||
// points: a use-after-move anywhere in the tree that happened to work before now sees nothing.
|
||||
explicit MultiPoint(Points &&_points) noexcept : points(std::move(_points)) {}
|
||||
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
|
||||
MultiPoint& operator=(MultiPoint &&other) noexcept { points = std::move(other.points); return *this; }
|
||||
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
|
||||
virtual ~MultiPoint() = default;
|
||||
void scale(double factor);
|
||||
void scale(double factor_x, double factor_y);
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#include <tuple>
|
||||
#include <unordered_set>
|
||||
#include <thread>
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <vector>
|
||||
#include "libslic3r.h"
|
||||
#include <utility>
|
||||
@@ -2585,490 +2583,467 @@ void PerimeterGenerator::process_arachne()
|
||||
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
|
||||
// we need to process each island separately because we might have different
|
||||
// extra perimeters for each one
|
||||
// Each island is independent up to its outputs, so they are generated in parallel - a layer split into thousands
|
||||
// of islands (e.g. by colour painting) otherwise ran on one thread - and the outputs are then committed in the
|
||||
// original island order, which is what the extra overhang perimeters (applied to the last island's loops and to
|
||||
// all fill surfaces so far) depend on.
|
||||
struct ArachneSurfaceResult
|
||||
{
|
||||
ExtrusionEntityCollection loops;
|
||||
bool has_loops = false;
|
||||
ExPolygons infill;
|
||||
ExPolygons no_overlap;
|
||||
};
|
||||
std::vector<ArachneSurfaceResult> results(all_surfaces.size());
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, all_surfaces.size()), [&](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) {
|
||||
const Surface &surface = all_surfaces[surface_idx];
|
||||
ArachneSurfaceResult &result = results[surface_idx];
|
||||
coord_t bead_width_0 = ext_perimeter_spacing;
|
||||
// detect how many perimeters must be generated for this island
|
||||
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
|
||||
int sparse_infill_density = this->config->sparse_infill_density.value;
|
||||
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
|
||||
loop_number++;
|
||||
for (const Surface& surface : all_surfaces) {
|
||||
coord_t bead_width_0 = ext_perimeter_spacing;
|
||||
// detect how many perimeters must be generated for this island
|
||||
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
|
||||
int sparse_infill_density = this->config->sparse_infill_density.value;
|
||||
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
|
||||
loop_number++;
|
||||
|
||||
// Set the bottommost layer to be one wall
|
||||
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
|
||||
if (is_bottom_layer && only_one_wall_first_layer)
|
||||
loop_number = 0;
|
||||
// Set the bottommost layer to be one wall
|
||||
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
|
||||
if (is_bottom_layer && only_one_wall_first_layer)
|
||||
loop_number = 0;
|
||||
|
||||
// Orca: set the topmost layer to be one wall according to the config
|
||||
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
|
||||
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
|
||||
loop_number = 0;
|
||||
// Orca: set the topmost layer to be one wall according to the config
|
||||
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
|
||||
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
|
||||
loop_number = 0;
|
||||
|
||||
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
|
||||
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
|
||||
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
|
||||
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
|
||||
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
|
||||
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
|
||||
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
|
||||
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
|
||||
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
|
||||
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
|
||||
|
||||
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
|
||||
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
|
||||
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
|
||||
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
|
||||
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
|
||||
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
|
||||
|
||||
coord_t wall_0_inset = 0;
|
||||
if (apply_precise_outer_wall)
|
||||
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
|
||||
coord_t wall_0_inset = 0;
|
||||
if (apply_precise_outer_wall)
|
||||
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
|
||||
|
||||
//PS: One wall top surface for Arachne
|
||||
ExPolygons top_expolygons;
|
||||
// Calculate how many inner loops remain when TopSurfaces is selected.
|
||||
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
|
||||
//PS: One wall top surface for Arachne
|
||||
ExPolygons top_expolygons;
|
||||
// Calculate how many inner loops remain when TopSurfaces is selected.
|
||||
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
|
||||
|
||||
// Set one perimeter when TopSurfaces is selected.
|
||||
if (only_one_wall_top && loop_number > 0)
|
||||
loop_number = 0;
|
||||
// Set one perimeter when TopSurfaces is selected.
|
||||
if (only_one_wall_top && loop_number > 0)
|
||||
loop_number = 0;
|
||||
|
||||
Arachne::WallToolPathsParams input_params_tmp = input_params;
|
||||
Arachne::WallToolPathsParams input_params_tmp = input_params;
|
||||
|
||||
Polygons last_p = to_polygons(last);
|
||||
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
|
||||
wall_0_inset, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
|
||||
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
|
||||
Polygons last_p = to_polygons(last);
|
||||
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
|
||||
wall_0_inset, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
|
||||
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
|
||||
|
||||
// Check if there are some remaining perimeters to generate (the number of perimeters
|
||||
// is greater than one together with enabled the single perimeter on top surface feature).
|
||||
if (inner_loop_number >= 0) {
|
||||
assert(upper_slices != nullptr);
|
||||
// Check if there are some remaining perimeters to generate (the number of perimeters
|
||||
// is greater than one together with enabled the single perimeter on top surface feature).
|
||||
if (inner_loop_number >= 0) {
|
||||
assert(upper_slices != nullptr);
|
||||
|
||||
coord_t perimeter_width = this->perimeter_flow.scaled_width();
|
||||
coord_t perimeter_width = this->perimeter_flow.scaled_width();
|
||||
|
||||
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
|
||||
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
|
||||
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
|
||||
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
|
||||
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
|
||||
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
|
||||
|
||||
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
|
||||
// contour uncovered, before bridges and too thin areas are filtered out.
|
||||
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
|
||||
// Contour bounding box.
|
||||
BoundingBox contour_bbox = get_extents(contour);
|
||||
contour_bbox.offset(SCALED_EPSILON);
|
||||
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
|
||||
// contour uncovered, before bridges and too thin areas are filtered out.
|
||||
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
|
||||
// Contour bounding box.
|
||||
BoundingBox contour_bbox = get_extents(contour);
|
||||
contour_bbox.offset(SCALED_EPSILON);
|
||||
|
||||
Polygons upper_slices_clipped;
|
||||
if (object_config->interface_shells) {
|
||||
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
|
||||
} else
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
|
||||
Polygons upper_slices_clipped;
|
||||
if (object_config->interface_shells) {
|
||||
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
|
||||
} else
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
|
||||
|
||||
ExPolygons top = diff_ex(contour, upper_slices_clipped);
|
||||
uncovered = !top.empty();
|
||||
if (top.empty())
|
||||
return top;
|
||||
ExPolygons top = diff_ex(contour, upper_slices_clipped);
|
||||
uncovered = !top.empty();
|
||||
if (top.empty())
|
||||
return top;
|
||||
|
||||
if (lower_slices != nullptr) {
|
||||
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
|
||||
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
|
||||
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
|
||||
if (lower_slices != nullptr) {
|
||||
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
|
||||
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
|
||||
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
|
||||
|
||||
// Remove bridges from top surface polygons.
|
||||
top = diff_ex(top, current_slices_bridges);
|
||||
}
|
||||
|
||||
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
|
||||
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
|
||||
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
|
||||
// due to thin lines being generated
|
||||
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
|
||||
|
||||
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
|
||||
return intersection_ex(top, contour);
|
||||
};
|
||||
|
||||
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
|
||||
std::vector<Arachne::VariableWidthLines> full_perimeters;
|
||||
Polygons full_inner_contour;
|
||||
bool full_perimeters_generated = false;
|
||||
auto generate_full_perimeters = [&]() {
|
||||
if (full_perimeters_generated)
|
||||
return;
|
||||
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
|
||||
full_perimeters = full_tool_paths.getToolPaths();
|
||||
full_inner_contour = full_tool_paths.getInnerContour();
|
||||
full_perimeters_generated = true;
|
||||
};
|
||||
|
||||
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
|
||||
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
|
||||
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
|
||||
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
|
||||
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
|
||||
// the width added, and only beads widened by more than twice the tolerance are looked for.
|
||||
const coord_t outer_wall_tolerance = bead_width_0 / 10;
|
||||
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
|
||||
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
|
||||
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
|
||||
bool nominal_uncovered = false;
|
||||
// Grown by an outer wall width to take in the outer walls bordering the top surface.
|
||||
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
|
||||
if (nominal_uncovered) {
|
||||
generate_full_perimeters();
|
||||
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
|
||||
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
|
||||
perimeters = { full_perimeters.front() };
|
||||
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
|
||||
}
|
||||
}
|
||||
// Remove bridges from top surface polygons.
|
||||
top = diff_ex(top, current_slices_bridges);
|
||||
}
|
||||
|
||||
bool uncovered = false;
|
||||
top_expolygons = get_top_expolygons(infill_contour, uncovered);
|
||||
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
|
||||
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
|
||||
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
|
||||
// due to thin lines being generated
|
||||
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
|
||||
|
||||
if (uncovered) {
|
||||
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
|
||||
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
|
||||
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
|
||||
// top-surface islands with inner walls that don't exist when the feature is disabled.
|
||||
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
|
||||
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
|
||||
: diff_ex(infill_contour, top_expolygons),
|
||||
wall_0_inset));
|
||||
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
|
||||
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
|
||||
return intersection_ex(top, contour);
|
||||
};
|
||||
|
||||
if (clip_walls_over_top) {
|
||||
Polygons kept_over_top;
|
||||
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
|
||||
// Route the top fill around the walls kept despite grazing the top.
|
||||
if (! kept_over_top.empty())
|
||||
top_expolygons = diff_ex(top_expolygons, kept_over_top);
|
||||
}
|
||||
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
|
||||
std::vector<Arachne::VariableWidthLines> full_perimeters;
|
||||
Polygons full_inner_contour;
|
||||
bool full_perimeters_generated = false;
|
||||
auto generate_full_perimeters = [&]() {
|
||||
if (full_perimeters_generated)
|
||||
return;
|
||||
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
|
||||
full_perimeters = full_tool_paths.getToolPaths();
|
||||
full_inner_contour = full_tool_paths.getInnerContour();
|
||||
full_perimeters_generated = true;
|
||||
};
|
||||
|
||||
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
|
||||
if (!perimeters.empty())
|
||||
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
|
||||
for (Arachne::ExtrusionLine &el : inner_perimeter)
|
||||
++el.inset_idx;
|
||||
|
||||
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
|
||||
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
|
||||
} else {
|
||||
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
|
||||
// feature is disabled.
|
||||
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
|
||||
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
|
||||
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
|
||||
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
|
||||
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
|
||||
// the width added, and only beads widened by more than twice the tolerance are looked for.
|
||||
const coord_t outer_wall_tolerance = bead_width_0 / 10;
|
||||
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
|
||||
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
|
||||
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
|
||||
bool nominal_uncovered = false;
|
||||
// Grown by an outer wall width to take in the outer walls bordering the top surface.
|
||||
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
|
||||
if (nominal_uncovered) {
|
||||
generate_full_perimeters();
|
||||
perimeters = std::move(full_perimeters);
|
||||
infill_contour = union_ex(full_inner_contour);
|
||||
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
|
||||
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
|
||||
perimeters = { full_perimeters.front() };
|
||||
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
|
||||
}
|
||||
}
|
||||
}
|
||||
//PS
|
||||
|
||||
loop_number = int(perimeters.size()) - 1;
|
||||
bool uncovered = false;
|
||||
top_expolygons = get_top_expolygons(infill_contour, uncovered);
|
||||
|
||||
#ifdef ARACHNE_DEBUG
|
||||
{
|
||||
static int iRun = 0;
|
||||
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
|
||||
if (uncovered) {
|
||||
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
|
||||
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
|
||||
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
|
||||
// top-surface islands with inner walls that don't exist when the feature is disabled.
|
||||
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
|
||||
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
|
||||
: diff_ex(infill_contour, top_expolygons),
|
||||
wall_0_inset));
|
||||
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
|
||||
|
||||
if (clip_walls_over_top) {
|
||||
Polygons kept_over_top;
|
||||
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
|
||||
// Route the top fill around the walls kept despite grazing the top.
|
||||
if (! kept_over_top.empty())
|
||||
top_expolygons = diff_ex(top_expolygons, kept_over_top);
|
||||
}
|
||||
|
||||
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
|
||||
if (!perimeters.empty())
|
||||
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
|
||||
for (Arachne::ExtrusionLine &el : inner_perimeter)
|
||||
++el.inset_idx;
|
||||
|
||||
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
|
||||
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
|
||||
} else {
|
||||
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
|
||||
// feature is disabled.
|
||||
generate_full_perimeters();
|
||||
perimeters = std::move(full_perimeters);
|
||||
infill_contour = union_ex(full_inner_contour);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
//PS
|
||||
|
||||
// All closed ExtrusionLine should have the same the first and the last point.
|
||||
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
|
||||
// equal the first and the last point.
|
||||
assert([&perimeters = std::as_const(perimeters)]() -> bool {
|
||||
for (const Arachne::VariableWidthLines& perimeter : perimeters)
|
||||
for (const Arachne::ExtrusionLine& el : perimeter)
|
||||
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
|
||||
return false;
|
||||
return true;
|
||||
}());
|
||||
loop_number = int(perimeters.size()) - 1;
|
||||
|
||||
int start_perimeter = int(perimeters.size()) - 1;
|
||||
int end_perimeter = -1;
|
||||
int direction = -1;
|
||||
#ifdef ARACHNE_DEBUG
|
||||
{
|
||||
static int iRun = 0;
|
||||
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner ||
|
||||
this->config->wall_sequence == WallSequence::InnerOuterInner;
|
||||
// All closed ExtrusionLine should have the same the first and the last point.
|
||||
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
|
||||
// equal the first and the last point.
|
||||
assert([&perimeters = std::as_const(perimeters)]() -> bool {
|
||||
for (const Arachne::VariableWidthLines& perimeter : perimeters)
|
||||
for (const Arachne::ExtrusionLine& el : perimeter)
|
||||
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
|
||||
return false;
|
||||
return true;
|
||||
}());
|
||||
|
||||
int start_perimeter = int(perimeters.size()) - 1;
|
||||
int end_perimeter = -1;
|
||||
int direction = -1;
|
||||
|
||||
bool is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner ||
|
||||
this->config->wall_sequence == WallSequence::InnerOuterInner;
|
||||
|
||||
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
||||
is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
|
||||
}
|
||||
if (is_outer_wall_first) {
|
||||
start_perimeter = 0;
|
||||
end_perimeter = int(perimeters.size());
|
||||
direction = 1;
|
||||
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
|
||||
is_outer_wall_first =
|
||||
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
|
||||
}
|
||||
if (is_outer_wall_first) {
|
||||
start_perimeter = 0;
|
||||
end_perimeter = int(perimeters.size());
|
||||
direction = 1;
|
||||
}
|
||||
|
||||
std::vector<Arachne::ExtrusionLine*> all_extrusions;
|
||||
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
|
||||
if (perimeters[perimeter_idx].empty())
|
||||
continue;
|
||||
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
|
||||
all_extrusions.emplace_back(&wall);
|
||||
}
|
||||
|
||||
// Find topological order with constraints from extrusions_constrains.
|
||||
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
|
||||
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
|
||||
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
|
||||
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
|
||||
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
|
||||
|
||||
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
|
||||
for (auto [before, after] : extrusions_constrains) {
|
||||
auto after_it = map_extrusion_to_idx.find(after);
|
||||
++blocked[after_it->second];
|
||||
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
|
||||
}
|
||||
|
||||
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
|
||||
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
|
||||
ordered_extrusions.reserve(all_extrusions.size());
|
||||
|
||||
while (ordered_extrusions.size() < all_extrusions.size()) {
|
||||
size_t best_candidate = 0;
|
||||
double best_distance_sqr = std::numeric_limits<double>::max();
|
||||
bool is_best_closed = false;
|
||||
|
||||
std::vector<size_t> available_candidates;
|
||||
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
|
||||
if (processed[candidate] || blocked[candidate])
|
||||
continue; // Not a valid candidate.
|
||||
available_candidates.push_back(candidate);
|
||||
}
|
||||
|
||||
std::vector<Arachne::ExtrusionLine*> all_extrusions;
|
||||
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
|
||||
if (perimeters[perimeter_idx].empty())
|
||||
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
|
||||
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
|
||||
});
|
||||
|
||||
for (const size_t candidate_path_idx : available_candidates) {
|
||||
auto& path = all_extrusions[candidate_path_idx];
|
||||
|
||||
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
|
||||
if (best_distance_sqr == std::numeric_limits<double>::max()) {
|
||||
best_candidate = candidate_path_idx;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
continue;
|
||||
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
|
||||
all_extrusions.emplace_back(&wall);
|
||||
}
|
||||
|
||||
// Find topological order with constraints from extrusions_constrains.
|
||||
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
|
||||
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
|
||||
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
|
||||
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
|
||||
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
|
||||
|
||||
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
|
||||
for (auto [before, after] : extrusions_constrains) {
|
||||
auto after_it = map_extrusion_to_idx.find(after);
|
||||
++blocked[after_it->second];
|
||||
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
|
||||
}
|
||||
|
||||
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
|
||||
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
|
||||
ordered_extrusions.reserve(all_extrusions.size());
|
||||
|
||||
while (ordered_extrusions.size() < all_extrusions.size()) {
|
||||
size_t best_candidate = 0;
|
||||
double best_distance_sqr = std::numeric_limits<double>::max();
|
||||
bool is_best_closed = false;
|
||||
|
||||
std::vector<size_t> available_candidates;
|
||||
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
|
||||
if (processed[candidate] || blocked[candidate])
|
||||
continue; // Not a valid candidate.
|
||||
available_candidates.push_back(candidate);
|
||||
}
|
||||
|
||||
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
|
||||
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
|
||||
});
|
||||
|
||||
for (const size_t candidate_path_idx : available_candidates) {
|
||||
auto& path = all_extrusions[candidate_path_idx];
|
||||
|
||||
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
|
||||
if (best_distance_sqr == std::numeric_limits<double>::max()) {
|
||||
best_candidate = candidate_path_idx;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const Point candidate_position = path->junctions.front().p;
|
||||
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
|
||||
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
|
||||
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
|
||||
best_candidate = candidate_path_idx;
|
||||
best_distance_sqr = distance_sqr;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
const Point candidate_position = path->junctions.front().p;
|
||||
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
|
||||
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
|
||||
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
|
||||
best_candidate = candidate_path_idx;
|
||||
best_distance_sqr = distance_sqr;
|
||||
is_best_closed = path->is_closed;
|
||||
}
|
||||
}
|
||||
|
||||
auto& best_path = all_extrusions[best_candidate];
|
||||
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
|
||||
processed[best_candidate] = true;
|
||||
for (size_t unlocked_idx : blocking[best_candidate])
|
||||
blocked[unlocked_idx]--;
|
||||
|
||||
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
|
||||
if (best_path->is_closed)
|
||||
current_position = best_path->junctions[0].p; //We end where we started.
|
||||
else
|
||||
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
|
||||
}
|
||||
}
|
||||
|
||||
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
|
||||
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
|
||||
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
|
||||
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
|
||||
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
|
||||
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
|
||||
|
||||
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
|
||||
// for OI mode that is used the basis for IOI
|
||||
bringContoursToFront(ordered_extrusions);
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
|
||||
|
||||
// Debug statement to print spacing values:
|
||||
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
|
||||
auto& best_path = all_extrusions[best_candidate];
|
||||
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
|
||||
processed[best_candidate] = true;
|
||||
for (size_t unlocked_idx : blocking[best_candidate])
|
||||
blocked[unlocked_idx]--;
|
||||
|
||||
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
|
||||
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
|
||||
coord_t threshold_external = (apply_precise_outer_wall)
|
||||
// Precise outer wall: use the full external spacing
|
||||
? ( this->ext_perimeter_flow.scaled_spacing()
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 )
|
||||
// Normal: half ext spacing plus half int spacing
|
||||
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 );
|
||||
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
|
||||
if (best_path->is_closed)
|
||||
current_position = best_path->junctions[0].p; //We end where we started.
|
||||
else
|
||||
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
|
||||
}
|
||||
}
|
||||
|
||||
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
|
||||
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
|
||||
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
|
||||
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
|
||||
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
|
||||
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
|
||||
|
||||
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
|
||||
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
|
||||
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
|
||||
// for OI mode that is used the basis for IOI
|
||||
bringContoursToFront(ordered_extrusions);
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
|
||||
|
||||
// Re-order extrusions based on distance
|
||||
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
|
||||
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
|
||||
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
|
||||
// Debug statement to print spacing values:
|
||||
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
|
||||
|
||||
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
|
||||
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
|
||||
coord_t threshold_external = (apply_precise_outer_wall)
|
||||
// Precise outer wall ⇒ use “full external spacing”
|
||||
? ( this->ext_perimeter_flow.scaled_spacing()
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 )
|
||||
// Normal ⇒ half ext spacing + half int spacing
|
||||
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
|
||||
+ this->perimeter_flow.scaled_spacing()/2.0 );
|
||||
|
||||
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
|
||||
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
|
||||
// We then advance the position index to move to the second island and continue until there are no more
|
||||
// perimeters left.
|
||||
while (position < reordered_extrusions.size()) {
|
||||
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
|
||||
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
|
||||
// run through the walls to get the index values that need re-ordering until the first one for each
|
||||
// is found. Start at "position" index to enable the for loop to iterate for multiple external
|
||||
// perimeters in a single island
|
||||
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
|
||||
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
|
||||
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
|
||||
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
|
||||
case 0: // external perimeter
|
||||
if (outer == -1)
|
||||
outer = arr_i;
|
||||
break;
|
||||
case 1: // first internal wall
|
||||
if (first_internal==-1 && arr_i>outer && outer!=-1){
|
||||
first_internal = arr_i;
|
||||
}
|
||||
break;
|
||||
case 2: // second internal wall
|
||||
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
|
||||
second_internal = arr_i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
|
||||
// This means we entered a new island.
|
||||
arr_i=arr_i-1; //step back one perimeter
|
||||
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
|
||||
break; // exit the for loop
|
||||
}
|
||||
}
|
||||
|
||||
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
|
||||
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
|
||||
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
|
||||
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
|
||||
|
||||
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
|
||||
if(arr_j >= second_internal){
|
||||
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
|
||||
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
|
||||
current_perimeter++;
|
||||
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
|
||||
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
|
||||
|
||||
// Re-order extrusions based on distance
|
||||
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
|
||||
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
|
||||
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
|
||||
|
||||
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
|
||||
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
|
||||
// We then advance the position index to move to the second island and continue until there are no more
|
||||
// perimeters left.
|
||||
while (position < reordered_extrusions.size()) {
|
||||
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
|
||||
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
|
||||
// run through the walls to get the index values that need re-ordering until the first one for each
|
||||
// is found. Start at "position" index to enable the for loop to iterate for multiple external
|
||||
// perimeters in a single island
|
||||
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
|
||||
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
|
||||
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
|
||||
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
|
||||
case 0: // external perimeter
|
||||
if (outer == -1)
|
||||
outer = arr_i;
|
||||
break;
|
||||
case 1: // first internal wall
|
||||
if (first_internal==-1 && arr_i>outer && outer!=-1){
|
||||
first_internal = arr_i;
|
||||
}
|
||||
}
|
||||
|
||||
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
|
||||
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
|
||||
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
|
||||
}
|
||||
|
||||
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
|
||||
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
|
||||
break;
|
||||
case 2: // second internal wall
|
||||
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
|
||||
second_internal = arr_i;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
|
||||
// This means we entered a new island.
|
||||
arr_i=arr_i-1; //step back one perimeter
|
||||
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
|
||||
break; // exit the for loop
|
||||
}
|
||||
// go to the next perimeter from the current position to continue scanning for external walls in the same island
|
||||
position = arr_i + 1;
|
||||
}
|
||||
|
||||
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
|
||||
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
|
||||
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
|
||||
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
|
||||
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
|
||||
|
||||
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
|
||||
if(arr_j >= second_internal){
|
||||
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
|
||||
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
|
||||
current_perimeter++;
|
||||
}
|
||||
}
|
||||
|
||||
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
|
||||
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
|
||||
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
|
||||
}
|
||||
|
||||
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
|
||||
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
|
||||
}
|
||||
// go to the next perimeter from the current position to continue scanning for external walls in the same island
|
||||
position = arr_i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool steep_overhang_contour = false;
|
||||
bool steep_overhang_hole = false;
|
||||
if (!config->overhang_reverse) {
|
||||
// Skip steep overhang detection no reverse is specified
|
||||
steep_overhang_contour = true;
|
||||
steep_overhang_hole = true;
|
||||
}
|
||||
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
|
||||
if (config->overhang_reverse) {
|
||||
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
||||
this->config->overhang_reverse_internal_only);
|
||||
}
|
||||
defer_unsupported_loops(*this, extrusion_coll);
|
||||
result.loops = std::move(extrusion_coll);
|
||||
result.has_loops = true;
|
||||
bool steep_overhang_contour = false;
|
||||
bool steep_overhang_hole = false;
|
||||
if (!config->overhang_reverse) {
|
||||
// Skip steep overhang detection no reverse is specified
|
||||
steep_overhang_contour = true;
|
||||
steep_overhang_hole = true;
|
||||
}
|
||||
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
|
||||
if (config->overhang_reverse) {
|
||||
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
||||
this->config->overhang_reverse_internal_only);
|
||||
}
|
||||
defer_unsupported_loops(*this, extrusion_coll);
|
||||
this->loops->append(extrusion_coll);
|
||||
}
|
||||
|
||||
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
|
||||
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
|
||||
|
||||
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
|
||||
infill_contour.clear(); // Infill region is too small, so let's filter it out.
|
||||
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
|
||||
infill_contour.clear(); // Infill region is too small, so let's filter it out.
|
||||
|
||||
// create one more offset to be used as boundary for fill
|
||||
// we offset by half the perimeter spacing (to get to the actual infill boundary)
|
||||
// and then we offset back and forth by half the infill spacing to only consider the
|
||||
// non-collapsing regions
|
||||
coord_t inset =
|
||||
(loop_number < 0) ? 0 :
|
||||
(loop_number == 0) ?
|
||||
// one loop
|
||||
ext_perimeter_spacing :
|
||||
// two or more loops?
|
||||
perimeter_spacing;
|
||||
coord_t top_inset = inset;
|
||||
// create one more offset to be used as boundary for fill
|
||||
// we offset by half the perimeter spacing (to get to the actual infill boundary)
|
||||
// and then we offset back and forth by half the infill spacing to only consider the
|
||||
// non-collapsing regions
|
||||
coord_t inset =
|
||||
(loop_number < 0) ? 0 :
|
||||
(loop_number == 0) ?
|
||||
// one loop
|
||||
ext_perimeter_spacing :
|
||||
// two or more loops?
|
||||
perimeter_spacing;
|
||||
coord_t top_inset = inset;
|
||||
|
||||
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
if(is_topmost_layer || is_bottom_layer)
|
||||
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
else
|
||||
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
if(is_topmost_layer || is_bottom_layer)
|
||||
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
else
|
||||
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
|
||||
|
||||
// simplify infill contours according to resolution
|
||||
Polygons pp;
|
||||
for (ExPolygon& ex : infill_contour)
|
||||
ex.simplify_p(m_scaled_resolution, &pp);
|
||||
ExPolygons not_filled_exp = union_ex(pp);
|
||||
// collapse too narrow infill areas
|
||||
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
|
||||
// simplify infill contours according to resolution
|
||||
Polygons pp;
|
||||
for (ExPolygon& ex : infill_contour)
|
||||
ex.simplify_p(m_scaled_resolution, &pp);
|
||||
ExPolygons not_filled_exp = union_ex(pp);
|
||||
// collapse too narrow infill areas
|
||||
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
|
||||
|
||||
ExPolygons infill_exp = offset2_ex(
|
||||
ExPolygons infill_exp = offset2_ex(
|
||||
not_filled_exp,
|
||||
float(-min_perimeter_infill_spacing / 2.),
|
||||
float(inset + min_perimeter_infill_spacing / 2.));
|
||||
// append infill areas to fill_surfaces
|
||||
if (!top_expolygons.empty()) {
|
||||
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
|
||||
}
|
||||
this->fill_surfaces->append(infill_exp, stInternal);
|
||||
|
||||
apply_extra_perimeters(infill_exp);
|
||||
|
||||
// BBS: get the no-overlap infill expolygons
|
||||
{
|
||||
ExPolygons polyWithoutOverlap;
|
||||
polyWithoutOverlap = offset2_ex(
|
||||
not_filled_exp,
|
||||
float(-min_perimeter_infill_spacing / 2.),
|
||||
float(inset + min_perimeter_infill_spacing / 2.));
|
||||
// append infill areas to fill_surfaces
|
||||
if (!top_expolygons.empty()) {
|
||||
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
|
||||
}
|
||||
result.infill = std::move(infill_exp);
|
||||
|
||||
// BBS: get the no-overlap infill expolygons
|
||||
{
|
||||
ExPolygons polyWithoutOverlap;
|
||||
polyWithoutOverlap = offset2_ex(
|
||||
not_filled_exp,
|
||||
float(-min_perimeter_infill_spacing / 2.),
|
||||
float(+min_perimeter_infill_spacing / 2.));
|
||||
if (!top_expolygons.empty())
|
||||
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
|
||||
result.no_overlap = std::move(polyWithoutOverlap);
|
||||
}
|
||||
}
|
||||
});
|
||||
for (ArachneSurfaceResult &result : results) {
|
||||
if (result.has_loops)
|
||||
// Moved, not copied: append(const ExtrusionEntity &) clones the whole wall tree of the island.
|
||||
this->loops->append(std::move(result.loops));
|
||||
this->fill_surfaces->append(result.infill, stInternal);
|
||||
apply_extra_perimeters(result.infill);
|
||||
append(*this->fill_no_overlap, std::move(result.no_overlap));
|
||||
float(+min_perimeter_infill_spacing / 2.));
|
||||
if (!top_expolygons.empty())
|
||||
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
|
||||
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
explicit Polygon(const Points &points) : MultiPoint(points) {}
|
||||
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
|
||||
Polygon(const Polygon &other) : MultiPoint(other.points) {}
|
||||
Polygon(Polygon &&other) noexcept : MultiPoint(std::move(other.points)) {}
|
||||
Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {}
|
||||
static Polygon new_scale(const std::vector<Vec2d> &points) {
|
||||
Polygon pgn;
|
||||
pgn.points.reserve(points.size());
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
return pgn;
|
||||
}
|
||||
Polygon& operator=(const Polygon &other) { points = other.points; return *this; }
|
||||
Polygon& operator=(Polygon &&other) noexcept { points = std::move(other.points); return *this; }
|
||||
Polygon& operator=(Polygon &&other) { points = std::move(other.points); return *this; }
|
||||
|
||||
Point& operator[](Points::size_type idx) { return this->points[idx]; }
|
||||
const Point& operator[](Points::size_type idx) const { return this->points[idx]; }
|
||||
|
||||
@@ -26,7 +26,7 @@ class Polyline : public MultiPoint {
|
||||
public:
|
||||
Polyline() {};
|
||||
Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {}
|
||||
Polyline(Polyline &&other) noexcept : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
|
||||
Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
|
||||
Polyline(std::initializer_list<Point> list) : MultiPoint(list) {
|
||||
fitting_result.clear();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public:
|
||||
fitting_result = other.fitting_result;
|
||||
return *this;
|
||||
}
|
||||
Polyline& operator=(Polyline&& other) noexcept {
|
||||
Polyline& operator=(Polyline&& other) {
|
||||
points = std::move(other.points);
|
||||
fitting_result = std::move(other.fitting_result);
|
||||
return *this;
|
||||
|
||||
@@ -13493,12 +13493,30 @@ static std::map<t_custom_gcode_key, t_config_option_keys> s_CustomGcodeSpecificP
|
||||
// Machine G-code
|
||||
{"file_start_gcode", {}},
|
||||
{"machine_start_gcode", {}},
|
||||
{"machine_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id"}},
|
||||
{"machine_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
|
||||
"current_filament_id", "current_extruder_id", "current_nozzle_id",
|
||||
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types"}},
|
||||
{"before_layer_change_gcode", {"layer_num", "layer_z", "max_layer_z"}},
|
||||
{"layer_change_gcode", {"layer_num", "layer_z", "max_layer_z"}},
|
||||
{"timelapse_gcode", {"layer_num", "layer_z", "max_layer_z"}},
|
||||
{"change_filament_gcode", {"layer_num", "layer_z", "max_layer_z", "next_extruder", "previous_extruder", "fan_speed",
|
||||
{"layer_change_gcode", {"layer_num", "layer_z", "max_layer_z", "most_used_physical_extruder_id",
|
||||
"current_filament_id", "current_nozzle_id", "curr_y_acceleration_limit",
|
||||
"curr_accumulated_mass", "curr_layer_mass"}},
|
||||
{"timelapse_gcode", {"layer_num", "layer_z", "max_layer_z", "most_used_physical_extruder_id",
|
||||
"curr_physical_extruder_id", "timelapse_pos_x", "timelapse_pos_y",
|
||||
"has_timelapse_safe_pos", "timelapse_inline_photo",
|
||||
"farthest_point_timelapse_enabled", "clear_to_x0"}},
|
||||
{"wrapping_detection_gcode", {"layer_num", "layer_z", "max_layer_z", "most_used_physical_extruder_id",
|
||||
"curr_physical_extruder_id"}},
|
||||
{"change_filament_gcode", {"layer_num", "layer_z", "max_layer_z", "next_extruder", "previous_extruder",
|
||||
"current_hotend", "next_hotend", "current_nozzle_id", "next_nozzle_id",
|
||||
"current_filament_id", "next_filament_id", "old_extruder_variant", "new_extruder_variant",
|
||||
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types", "filament_retract_length_nc",
|
||||
"new_extruder_retracted_length", "outer_wall_volumetric_speed", "fan_speed",
|
||||
"first_flush_volume", "flush_length_1", "flush_length_2", "flush_length_3", "flush_length_4",
|
||||
"flush_length", "flush_volumetric_speeds", "flush_temperatures", "filament_cooling_before_tower",
|
||||
"wipe_avoid_perimeter", "wipe_avoid_pos_x", "is_prime_tower_interface",
|
||||
"filament_tower_interface_purge_volume", "filament_tower_interface_print_temp",
|
||||
"wipe_tower_center_pos_x", "wipe_tower_center_pos_y", "wipe_tower_center_pos_valid",
|
||||
"temperature", "nozzle_temperature", "first_layer_temperature", "nozzle_temperature_initial_layer",
|
||||
"new_filament_e_feedrate", "new_filament_temp", "new_retract_length",
|
||||
"new_retract_length_toolchange", "old_filament_e_feedrate", "old_filament_temp", "old_retract_length",
|
||||
"old_retract_length_toolchange", "relative_e_axis", "second_flush_volume", "toolchange_count", "toolchange_z",
|
||||
@@ -13511,8 +13529,18 @@ static std::map<t_custom_gcode_key, t_config_option_keys> s_CustomGcodeSpecificP
|
||||
{"machine_pause_gcode", {}},
|
||||
{"template_custom_gcode", {}},
|
||||
// Filament G-code
|
||||
{"filament_start_gcode", {"filament_extruder_id"}},
|
||||
{"filament_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id"}},
|
||||
{"filament_start_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
|
||||
"current_filament_id", "current_extruder_id", "current_nozzle_id",
|
||||
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types",
|
||||
"retraction_distance_when_cut", "long_retraction_when_cut",
|
||||
"temperature", "nozzle_temperature", "first_layer_temperature",
|
||||
"nozzle_temperature_initial_layer"}},
|
||||
{"filament_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
|
||||
"current_filament_id", "current_extruder_id", "current_nozzle_id",
|
||||
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types",
|
||||
"retraction_distance_when_cut", "long_retraction_when_cut",
|
||||
"temperature", "nozzle_temperature", "first_layer_temperature",
|
||||
"nozzle_temperature_initial_layer"}},
|
||||
};
|
||||
|
||||
const std::map<t_custom_gcode_key, t_config_option_keys>& custom_gcode_specific_placeholders()
|
||||
@@ -13541,11 +13569,44 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
|
||||
def->label = L("Filament extruder ID");
|
||||
def->tooltip = L("The current extruder ID. The same as current_extruder.");
|
||||
|
||||
new_def("current_filament_id", coInt, L("Current filament ID"), L("Index of the currently active filament. The index is zero-based (first filament has index 0)."));
|
||||
new_def("current_extruder_id", coInt, L("Current extruder ID"), L("Index of the currently active physical extruder. The index is zero-based (first extruder has index 0)."));
|
||||
new_def("current_nozzle_id", coInt, L("Current nozzle ID"), L("Index of the currently active nozzle. The index is zero-based (first nozzle has index 0)."));
|
||||
new_def("nozzle_diameter_at_nozzle_id", coFloats, L("Nozzle diameters"), L("Nozzle diameters indexed by nozzle ID."));
|
||||
new_def("nozzle_volume_types", coStrings, L("Nozzle volume types"), L("Nozzle volume variants indexed by nozzle ID."));
|
||||
new_def("retraction_distance_when_cut", coFloat, L("Retraction distance when cut"), L("Retraction distance used when cutting filament."));
|
||||
new_def("long_retraction_when_cut", coBool, L("Long retraction when cut"), L("Whether long retraction is used when cutting filament."));
|
||||
|
||||
// layer_change_gcode
|
||||
new_def("most_used_physical_extruder_id", coInt, L("Most-used physical extruder ID"), L("Index of the physical extruder used most often on the current layer. The index is zero-based (first extruder has index 0)."));
|
||||
new_def("curr_y_acceleration_limit", coFloat, L("Current Y acceleration limit"), L("Current Y-axis acceleration limit after the bed-mass adjustment."));
|
||||
new_def("curr_accumulated_mass", coFloat, L("Current accumulated mass"), L("Accumulated printed mass at the current layer."));
|
||||
new_def("curr_layer_mass", coFloat, L("Current layer mass"), L("Printed mass of the previous layer."));
|
||||
|
||||
// timelapse_gcode
|
||||
new_def("curr_physical_extruder_id", coInt, L("Current physical extruder ID"), L("Index of the physical extruder active for the current layer. The index is zero-based (first extruder has index 0)."));
|
||||
new_def("timelapse_pos_x", coInt, L("Timelapse position X"), L("Selected X position for timelapse capture."));
|
||||
new_def("timelapse_pos_y", coInt, L("Timelapse position Y"), L("Selected Y position for timelapse capture."));
|
||||
new_def("has_timelapse_safe_pos", coBool, L("Has timelapse safe position"), L("Whether a safe timelapse position was found."));
|
||||
new_def("timelapse_inline_photo", coBool, L("Timelapse inline photo"), L("Whether the timelapse photo is captured at the inline position."));
|
||||
new_def("farthest_point_timelapse_enabled", coBool, L("Farthest-point timelapse enabled"), L("Whether farthest-point timelapse positioning is active."));
|
||||
new_def("clear_to_x0", coBool, L("Clear to X0"), L("Whether the timelapse move is clear to X0."));
|
||||
|
||||
// change_filament_gcode
|
||||
new_def("previous_extruder", coInt, L("Previous extruder"), L("Index of the extruder that is being unloaded. The index is zero based (first extruder has index 0)."));
|
||||
new_def("next_extruder", coInt, L("Next extruder"), L("Index of the extruder that is being loaded. The index is zero based (first extruder has index 0)."));
|
||||
new_def("current_hotend", coInt, L("Current hotend"), L("Zero-based extruder index of the outgoing filament. On Bambu printers it is -1, or the nozzle ID when a filament moves between nozzles across layers."));
|
||||
new_def("next_hotend", coInt, L("Next hotend"), L("Zero-based extruder index of the incoming filament. On Bambu printers it is -1, or the nozzle ID when a filament moves between nozzles across layers."));
|
||||
new_def("next_nozzle_id", coInt, L("Next nozzle ID"), L("Index of the nozzle associated with the incoming filament. The index is zero-based (first nozzle has index 0)."));
|
||||
new_def("next_filament_id", coInt, L("Next filament ID"), L("Index of the incoming filament. The index is zero-based (first filament has index 0)."));
|
||||
new_def("old_extruder_variant", coString, L("Old extruder variant"), L("Nozzle-volume variant of the outgoing extruder."));
|
||||
new_def("new_extruder_variant", coString, L("New extruder variant"), L("Nozzle-volume variant of the incoming extruder."));
|
||||
new_def("filament_retract_length_nc", coFloat, L("Filament nozzle-change retract length"), L("Nozzle-change retraction length of the outgoing filament."));
|
||||
new_def("new_extruder_retracted_length", coFloat, L("New extruder retracted length"), L("Currently parked retraction length of the incoming extruder."));
|
||||
new_def("outer_wall_volumetric_speed", coFloat, L("Outer wall volumetric speed"), L("Volumetric speed used for the outer wall."));
|
||||
new_def("relative_e_axis", coBool, L("Relative e-axis"), L("Indicates if relative positioning is being used."));
|
||||
new_def("toolchange_count", coInt, L("Toolchange count"), L("The number of toolchanges throughout the print."));
|
||||
new_def("toolchange_z", coFloat, L("Toolchange Z"), L("The Z position during the toolchange."));
|
||||
new_def("fan_speed", coNone, "", ""); //Option is no longer used and is zeroed by placeholder parser for compatability
|
||||
new_def("old_retract_length", coFloat, L("Old retract length"), L("The retraction length of the previous filament."));
|
||||
new_def("new_retract_length", coFloat, L("New retract length"), L("The retraction length of the new filament."));
|
||||
@@ -13570,6 +13631,22 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
|
||||
new_def("flush_length_2", coFloat, L("Flush Length 2"), L("The second flush length."));
|
||||
new_def("flush_length_3", coFloat, L("Flush Length 3"), L("The third flush length."));
|
||||
new_def("flush_length_4", coFloat, L("Flush Length 4"), L("The fourth flush length."));
|
||||
new_def("flush_length", coFloat, L("Flush length"), L("Total flush length."));
|
||||
new_def("flush_volumetric_speeds", coFloats, L("Flush volumetric speeds"), L("Flush volumetric speeds for each filament."));
|
||||
new_def("flush_temperatures", coInts, L("Flush temperatures"), L("Flush temperatures for each filament."));
|
||||
new_def("filament_cooling_before_tower", coFloats, L("Filament cooling before tower"), L("Temperature drop before entering filament tower"));
|
||||
new_def("wipe_avoid_perimeter", coBool, L("Avoid wipe perimeter"), L("Whether travel should avoid the wipe perimeter."));
|
||||
new_def("wipe_avoid_pos_x", coFloat, L("Wipe avoidance X"), L("X position used to avoid the wipe area."));
|
||||
new_def("is_prime_tower_interface", coBool, L("Prime tower interface"), L("Whether the current purge is for a prime-tower interface."));
|
||||
new_def("filament_tower_interface_purge_volume", coFloat, L("Interface layer purge length"), L("Purge length for prime tower interface layer (where different materials meet)."));
|
||||
new_def("filament_tower_interface_print_temp", coInt, L("Filament tower interface print temperature"), L("Print temperature used for the filament tower interface."));
|
||||
new_def("wipe_tower_center_pos_x", coFloat, L("Wipe tower center X"), L("X coordinate of the nozzle's wipe-tower wait position, 2 mm outside the tower edge."));
|
||||
new_def("wipe_tower_center_pos_y", coFloat, L("Wipe tower center Y"), L("Y coordinate of the nozzle's wipe-tower wait position."));
|
||||
new_def("wipe_tower_center_pos_valid", coBool, L("Wipe tower center position valid"), L("Whether the print has a wipe tower."));
|
||||
new_def("temperature", coInts, L("Temperature"), L("Nozzle temperatures indexed by filament."));
|
||||
new_def("nozzle_temperature", coInts, L("Nozzle temperature"), L("Nozzle temperatures indexed by filament."));
|
||||
new_def("first_layer_temperature", coInts, L("First layer temperature"), L("First-layer nozzle temperatures indexed by filament."));
|
||||
new_def("nozzle_temperature_initial_layer", coInts, L("Nozzle temperature initial layer"), L("First-layer nozzle temperatures indexed by filament."));
|
||||
|
||||
// change_extrusion_role_gcode
|
||||
// The whole tooltip is one literal so that the GUI can look it up in the catalog.
|
||||
|
||||
+184
-352
@@ -50,7 +50,6 @@
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <float.h>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <ios>
|
||||
#include <iomanip>
|
||||
@@ -77,7 +76,6 @@
|
||||
#include <Eigen/Core>
|
||||
|
||||
#include <tbb/parallel_for.h>
|
||||
#include <tbb/parallel_invoke.h>
|
||||
#include <tbb/spin_mutex.h>
|
||||
#include <tbb/concurrent_unordered_set.h>
|
||||
|
||||
@@ -1932,9 +1930,7 @@ void PrintObject::detect_surfaces_type()
|
||||
bool interface_shells = ! spiral_mode && m_config.interface_shells.value;
|
||||
size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size();
|
||||
|
||||
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
|
||||
// others, so the regions run next to each other instead of one after another, each still over all layers.
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start";
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
for (Layer *layer : m_layers)
|
||||
@@ -1992,7 +1988,7 @@ void PrintObject::detect_surfaces_type()
|
||||
if (upper_layer) {
|
||||
ExPolygons upper_slices = interface_shells ?
|
||||
diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) :
|
||||
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(upper_layer->lslices), ApplySafetyOffset::Yes);
|
||||
diff_ex(layerm_slices_surfaces, upper_layer->lslices, ApplySafetyOffset::Yes);
|
||||
surfaces_append(top, opening_ex(upper_slices, offset), stTop);
|
||||
} else {
|
||||
// if no upper layer, all surfaces of this one are solid
|
||||
@@ -2018,7 +2014,7 @@ void PrintObject::detect_surfaces_type()
|
||||
surfaces_append(
|
||||
bottom,
|
||||
opening_ex(
|
||||
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(lower_layer->lslices), ApplySafetyOffset::Yes),
|
||||
diff_ex(layerm_slices_surfaces, lower_layer->lslices, ApplySafetyOffset::Yes),
|
||||
offset),
|
||||
surface_type_bottom_other);
|
||||
// if user requested internal shells, we need to identify surfaces
|
||||
@@ -2049,44 +2045,34 @@ void PrintObject::detect_surfaces_type()
|
||||
// and top surfaces; let's do an intersection to discover them and consider them
|
||||
// as bottom surfaces (to allow for bridge detection)
|
||||
if (! top.empty() && ! bottom.empty()) {
|
||||
const auto cracks = intersection_ex_by_piece(to_expolygons(top), to_polygons(bottom));
|
||||
const auto cracks = intersection_ex(top, bottom);
|
||||
if (!cracks.empty()) {
|
||||
if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom
|
||||
const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5;
|
||||
|
||||
// Only the bottom surfaces near a crack can take part: one that contains it must contain its box,
|
||||
// and one whose box misses the grown crack is left unchanged by removing it. A layer cut through
|
||||
// a fine relief has thousands of both, which made this loop quadratic.
|
||||
for (const auto& crack : cracks) {
|
||||
if (offset_ex(crack, small_crack_threshold).empty()) {
|
||||
// For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well
|
||||
const BoundingBox crack_bbox = get_extents(crack);
|
||||
if (std::any_of(bottom.begin(), bottom.end(), [&crack, &crack_bbox, small_crack_threshold](const Surface& s) {
|
||||
if (std::any_of(bottom.begin(), bottom.end(), [&crack, small_crack_threshold](const Surface& s) {
|
||||
const auto& se = s.expolygon;
|
||||
return get_extents(se).inflated(SCALED_EPSILON).contains(crack_bbox)
|
||||
&& diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
|
||||
return diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
|
||||
&& se.area() > crack.area() * 2
|
||||
&& !offset_ex(diff_ex(se, crack), small_crack_threshold).empty();
|
||||
})) continue;
|
||||
|
||||
// Crack too small, leave it as part of the top surface, remove it from bottom surfaces
|
||||
const ExPolygons grown_crack = offset_ex(crack, -small_crack_threshold);
|
||||
const BoundingBox grown_bbox = get_extents(grown_crack);
|
||||
Surfaces bot_tmp;
|
||||
for (auto& b : bottom) {
|
||||
if (get_extents(b.expolygon).overlap(grown_bbox))
|
||||
surfaces_append(bot_tmp, diff_ex(b.expolygon, grown_crack), b.surface_type);
|
||||
else
|
||||
bot_tmp.emplace_back(std::move(b));
|
||||
surfaces_append(bot_tmp, diff_ex(b.expolygon, offset_ex(crack, -small_crack_threshold)), b.surface_type);
|
||||
}
|
||||
bottom = std::move(bot_tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExPolygons top_expolygons = to_expolygons(std::move(top));
|
||||
Polygons top_polygons = to_polygons(std::move(top));
|
||||
top.clear();
|
||||
surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop);
|
||||
surfaces_append(top, diff_ex(top_polygons, bottom), stTop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2177,7 +2163,7 @@ void PrintObject::detect_surfaces_type()
|
||||
{
|
||||
Polygons topbottom = to_polygons(top);
|
||||
polygons_append(topbottom, to_polygons(bottom));
|
||||
surfaces_append(surfaces_out, diff_ex_by_piece(surfaces_prev_expolys, topbottom), stInternal);
|
||||
surfaces_append(surfaces_out, diff_ex(surfaces_prev_expolys, topbottom), stInternal);
|
||||
}
|
||||
|
||||
surfaces_append(surfaces_out, std::move(top));
|
||||
@@ -2354,31 +2340,29 @@ void PrintObject::detect_surfaces_type()
|
||||
}
|
||||
}
|
||||
);
|
||||
// ==============================================================================================================
|
||||
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
|
||||
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
|
||||
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
|
||||
// ==============================================================================================================
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
|
||||
tbb::parallel_for( tbb::blocked_range<size_t>(0, m_layers.size()), [this, region_id](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
|
||||
Surfaces &surfs = m_layers[idx_layer]->m_regions[region_id]->slices.surfaces;
|
||||
for (Surface &s : surfs) {
|
||||
if (s.surface_type == stInternalAfterExternalBridge) {
|
||||
s.surface_type = stBottomBridge;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// ==============================================================================================================
|
||||
// === ORCA: End of second external bridge layer changes =======================================================
|
||||
// ==============================================================================================================
|
||||
|
||||
}); // for each this->print->region_count
|
||||
|
||||
// ==============================================================================================================
|
||||
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
|
||||
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
|
||||
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
|
||||
// ==============================================================================================================
|
||||
// Once all the regions have their second bridge layer, and before their slices are trimmed into fill surfaces below.
|
||||
if ((this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)) {
|
||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_layers.size()), [this](const tbb::blocked_range<size_t> &range) {
|
||||
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer)
|
||||
for (LayerRegion *layerm : m_layers[idx_layer]->regions())
|
||||
for (Surface &s : layerm->slices.surfaces)
|
||||
if (s.surface_type == stInternalAfterExternalBridge)
|
||||
s.surface_type = stBottomBridge;
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
}
|
||||
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start";
|
||||
// Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
|
||||
tbb::parallel_for(
|
||||
@@ -2395,7 +2379,7 @@ void PrintObject::detect_surfaces_type()
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end";
|
||||
});
|
||||
} // for each this->print->region_count
|
||||
|
||||
// Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.)
|
||||
m_typed_slices = true;
|
||||
@@ -2462,10 +2446,8 @@ void PrintObject::process_external_surfaces()
|
||||
BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end";
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - start";
|
||||
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
|
||||
// others, so the regions run next to each other instead of one after another, each still over all layers.
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), [this, &surfaces_covered](size_t region_id) {
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - start";
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, m_layers.size()),
|
||||
[this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) {
|
||||
@@ -2480,9 +2462,9 @@ void PrintObject::process_external_surfaces()
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - end";
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - end";
|
||||
}
|
||||
}
|
||||
|
||||
void PrintObject::discover_vertical_shells()
|
||||
@@ -2521,10 +2503,10 @@ void PrintObject::discover_vertical_shells()
|
||||
// The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit.
|
||||
return;
|
||||
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom";
|
||||
// One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a
|
||||
// few such layers next to each other must not end up in one task.
|
||||
//FIXME Improve the heuristics for a grain size.
|
||||
size_t grain_size = std::max(num_layers / 16, size_t(1));
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, num_layers, 1),
|
||||
tbb::blocked_range<size_t>(0, num_layers, grain_size),
|
||||
[this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) {
|
||||
const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge };
|
||||
const size_t num_regions = this->num_printing_regions();
|
||||
@@ -2532,198 +2514,67 @@ void PrintObject::discover_vertical_shells()
|
||||
m_print->throw_if_canceled();
|
||||
const Layer &layer = *m_layers[idx_layer];
|
||||
DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer];
|
||||
const auto top_bottom_expansion = [&layer](size_t region_id) {
|
||||
return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
|
||||
};
|
||||
// Simulate single set of perimeters over all merged regions.
|
||||
float perimeter_offset = 0.f;
|
||||
float perimeter_min_spacing = FLT_MAX;
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
static size_t debug_idx = 0;
|
||||
++ debug_idx;
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
// The top surfaces, the bottom surfaces and the holes are independent of each other.
|
||||
tbb::parallel_invoke(
|
||||
[&]() {
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
|
||||
append(cache.top_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_type(stTop), top_bottom_expansion(region_id)));
|
||||
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
|
||||
// Save some computing time by reducing the number of polygons.
|
||||
cache.top_surfaces = union_(cache.top_surfaces);
|
||||
},
|
||||
[&]() {
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
|
||||
append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id)));
|
||||
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
|
||||
cache.bottom_surfaces = union_(cache.bottom_surfaces);
|
||||
},
|
||||
[&]() {
|
||||
// Simulate single set of perimeters over all merged regions.
|
||||
float perimeter_offset = 0.f;
|
||||
float perimeter_min_spacing = FLT_MAX;
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
|
||||
const LayerRegion &layerm = *layer.m_regions[region_id];
|
||||
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
|
||||
// First find the maxium number of perimeters per region slice.
|
||||
unsigned int perimeters = 0;
|
||||
for (const Surface &s : layerm.slices.surfaces)
|
||||
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
|
||||
perimeters += layerm.region().config().wall_loops.value;
|
||||
// Then calculate the infill offset.
|
||||
if (perimeters > 0) {
|
||||
Flow extflow = layerm.flow(frExternalPerimeter);
|
||||
Flow flow = layerm.flow(frPerimeter);
|
||||
perimeter_offset = std::max(perimeter_offset,
|
||||
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
|
||||
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
|
||||
}
|
||||
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
|
||||
}
|
||||
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
|
||||
if (perimeter_offset > 0.) {
|
||||
// The layer.lslices are forced to merge by expanding them first.
|
||||
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
|
||||
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
|
||||
LayerRegion &layerm = *layer.m_regions[region_id];
|
||||
float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
|
||||
// Top surfaces.
|
||||
append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion));
|
||||
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
|
||||
// Bottom surfaces.
|
||||
append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion));
|
||||
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
|
||||
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
|
||||
// First find the maxium number of perimeters per region slice.
|
||||
unsigned int perimeters = 0;
|
||||
for (Surface &s : layerm.slices.surfaces)
|
||||
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
|
||||
perimeters += layerm.region().config().wall_loops.value;
|
||||
// Then calculate the infill offset.
|
||||
if (perimeters > 0) {
|
||||
Flow extflow = layerm.flow(frExternalPerimeter);
|
||||
Flow flow = layerm.flow(frPerimeter);
|
||||
perimeter_offset = std::max(perimeter_offset,
|
||||
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
|
||||
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
|
||||
}
|
||||
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
|
||||
}
|
||||
// Save some computing time by reducing the number of polygons.
|
||||
cache.top_surfaces = union_(cache.top_surfaces);
|
||||
cache.bottom_surfaces = union_(cache.bottom_surfaces);
|
||||
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
|
||||
if (perimeter_offset > 0.) {
|
||||
// The layer.lslices are forced to merge by expanding them first.
|
||||
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
|
||||
svg.draw(layer.lslices, "blue");
|
||||
svg.draw(union_ex(cache.holes), "red");
|
||||
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
|
||||
svg.Close();
|
||||
}
|
||||
{
|
||||
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
|
||||
svg.draw(layer.lslices, "blue");
|
||||
svg.draw(union_ex(cache.holes), "red");
|
||||
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
|
||||
svg.Close();
|
||||
}
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
}
|
||||
cache.holes = union_(cache.holes);
|
||||
});
|
||||
}
|
||||
cache.holes = union_(cache.holes);
|
||||
}
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom";
|
||||
}
|
||||
|
||||
// With one top/bottom cache for all regions, the shell and hole accumulation in the loop below depends on nothing
|
||||
// region-specific but the shell settings and the external perimeter spacing, so a region sharing them with an earlier
|
||||
// one reuses its result instead of repeating it: that accumulation is a union over several layers of top/bottom
|
||||
// surfaces, and a multi-material print has a region per filament.
|
||||
using AccumulationKey = std::array<double, 5>;
|
||||
struct ShellAccumulation
|
||||
{
|
||||
AccumulationKey key;
|
||||
Polygons shell;
|
||||
Polygons holes;
|
||||
};
|
||||
const auto accumulation_key = [](const PrintRegionConfig ®ion_config, const LayerRegion *layerm) {
|
||||
return AccumulationKey{ double(region_config.top_shell_layers.value), region_config.top_shell_thickness.value,
|
||||
double(region_config.bottom_shell_layers.value), region_config.bottom_shell_thickness.value,
|
||||
double(layerm->flow(frExternalPerimeter).scaled_spacing()) };
|
||||
};
|
||||
const auto accumulate_shell = [this, &cache_top_botom_regions](size_t idx_layer, const PrintRegionConfig ®ion_config,
|
||||
const LayerRegion *layerm, Polygons &shell, Polygons &holes) {
|
||||
const Layer *layer = m_layers[idx_layer];
|
||||
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
|
||||
auto combine_holes = [&holes](const Polygons &holes2) {
|
||||
if (holes.empty() || holes2.empty())
|
||||
holes.clear();
|
||||
else
|
||||
holes = intersection(holes, holes2);
|
||||
};
|
||||
auto combine_shells = [&shell](const Polygons &shells2) {
|
||||
if (shell.empty())
|
||||
shell = std::move(shells2);
|
||||
else if (! shells2.empty()) {
|
||||
polygons_append(shell, shells2);
|
||||
// Running the union_ using the Clipper library piece by piece is cheaper
|
||||
// than running the union_ all at once.
|
||||
shell = union_(shell);
|
||||
}
|
||||
};
|
||||
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
|
||||
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
|
||||
// Gather top regions projected to this layer.
|
||||
coordf_t print_z = layer->print_z;
|
||||
int i = int(idx_layer) + 1;
|
||||
int itop = int(idx_layer) + n_top_layers;
|
||||
bool at_least_one_top_projected = false;
|
||||
for (; i < int(cache_top_botom_regions.size()) &&
|
||||
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
|
||||
++ i) {
|
||||
at_least_one_top_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.top_surfaces);
|
||||
}
|
||||
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
|
||||
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
|
||||
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
|
||||
// perimeter width of area
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i < int(cache_top_botom_regions.size()) &&
|
||||
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
|
||||
// Gather bottom regions projected to this layer.
|
||||
coordf_t bottom_z = layer->bottom_z();
|
||||
int i = int(idx_layer) - 1;
|
||||
int ibottom = int(idx_layer) - n_bottom_layers;
|
||||
bool at_least_one_bottom_projected = false;
|
||||
for (; i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
|
||||
-- i) {
|
||||
at_least_one_bottom_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.bottom_surfaces);
|
||||
}
|
||||
|
||||
if (!at_least_one_bottom_projected && i >= 0) {
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
};
|
||||
std::vector<std::vector<ShellAccumulation>> shell_accumulations(top_bottom_surfaces_all_regions ? num_layers : 0);
|
||||
if (! shell_accumulations.empty()) {
|
||||
// Every (layer, key) pair is accumulated once, before the regions, so that nothing in the loop below is shared
|
||||
// between them and they can run next to each other.
|
||||
std::vector<std::array<size_t, 3>> todo; // layer, its slot, a region holding the key
|
||||
for (size_t idx_layer = 0; idx_layer < num_layers; ++ idx_layer) {
|
||||
std::vector<ShellAccumulation> &accumulations = shell_accumulations[idx_layer];
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
|
||||
if (this->printing_region(region_id).config().ensure_vertical_shell_thickness.value != evstAll)
|
||||
continue;
|
||||
const LayerRegion *layerm = m_layers[idx_layer]->m_regions[region_id];
|
||||
const AccumulationKey key = accumulation_key(layerm->region().config(), layerm);
|
||||
if (std::none_of(accumulations.begin(), accumulations.end(), [&key](const ShellAccumulation &a) { return a.key == key; })) {
|
||||
todo.push_back({ idx_layer, accumulations.size(), region_id });
|
||||
accumulations.push_back({ key, {}, {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
tbb::parallel_for(size_t(0), todo.size(), [this, &todo, &shell_accumulations, &accumulate_shell](size_t i) {
|
||||
m_print->throw_if_canceled();
|
||||
const LayerRegion *layerm = m_layers[todo[i][0]]->m_regions[todo[i][2]];
|
||||
ShellAccumulation &out = shell_accumulations[todo[i][0]][todo[i][1]];
|
||||
accumulate_shell(todo[i][0], layerm->region().config(), layerm, out.shell, out.holes);
|
||||
});
|
||||
m_print->throw_if_canceled();
|
||||
}
|
||||
|
||||
const auto process_region = [&](size_t region_id) {
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
|
||||
const PrintRegion ®ion = this->printing_region(region_id);
|
||||
if (region.config().ensure_vertical_shell_thickness.value != evstAll )
|
||||
// This region will be handled by discover_horizontal_shells().
|
||||
return;
|
||||
continue;
|
||||
|
||||
//FIXME Improve the heuristics for a grain size.
|
||||
size_t grain_size = std::max(num_layers / 16, size_t(1));
|
||||
@@ -2763,7 +2614,7 @@ void PrintObject::discover_vertical_shells()
|
||||
grain_size = 1;
|
||||
tbb::parallel_for(
|
||||
tbb::blocked_range<size_t>(0, num_layers, grain_size),
|
||||
[this, region_id, &shell_accumulations, &accumulation_key, &accumulate_shell]
|
||||
[this, region_id, &cache_top_botom_regions]
|
||||
(const tbb::blocked_range<size_t>& range) {
|
||||
// printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end());
|
||||
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
|
||||
@@ -2813,19 +2664,80 @@ void PrintObject::discover_vertical_shells()
|
||||
}
|
||||
}
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
const AccumulationKey key = accumulation_key(region_config, layerm);
|
||||
const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr :
|
||||
[&]() -> const ShellAccumulation * {
|
||||
for (const ShellAccumulation &a : shell_accumulations[idx_layer])
|
||||
if (a.key == key)
|
||||
return &a;
|
||||
return nullptr;
|
||||
}();
|
||||
if (reused != nullptr) {
|
||||
shell = reused->shell;
|
||||
holes = reused->holes;
|
||||
} else
|
||||
accumulate_shell(idx_layer, region_config, layerm, shell, holes);
|
||||
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
|
||||
auto combine_holes = [&holes](const Polygons &holes2) {
|
||||
if (holes.empty() || holes2.empty())
|
||||
holes.clear();
|
||||
else
|
||||
holes = intersection(holes, holes2);
|
||||
};
|
||||
auto combine_shells = [&shell](const Polygons &shells2) {
|
||||
if (shell.empty())
|
||||
shell = std::move(shells2);
|
||||
else if (! shells2.empty()) {
|
||||
polygons_append(shell, shells2);
|
||||
// Running the union_ using the Clipper library piece by piece is cheaper
|
||||
// than running the union_ all at once.
|
||||
shell = union_(shell);
|
||||
}
|
||||
};
|
||||
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
|
||||
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
|
||||
// Gather top regions projected to this layer.
|
||||
coordf_t print_z = layer->print_z;
|
||||
int i = int(idx_layer) + 1;
|
||||
int itop = int(idx_layer) + n_top_layers;
|
||||
bool at_least_one_top_projected = false;
|
||||
for (; i < int(cache_top_botom_regions.size()) &&
|
||||
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
|
||||
++ i) {
|
||||
at_least_one_top_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.top_surfaces);
|
||||
}
|
||||
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
|
||||
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
|
||||
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
|
||||
// perimeter width of area
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i < int(cache_top_botom_regions.size()) &&
|
||||
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
|
||||
// Gather bottom regions projected to this layer.
|
||||
coordf_t bottom_z = layer->bottom_z();
|
||||
int i = int(idx_layer) - 1;
|
||||
int ibottom = int(idx_layer) - n_bottom_layers;
|
||||
bool at_least_one_bottom_projected = false;
|
||||
for (; i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
|
||||
-- i) {
|
||||
at_least_one_bottom_projected = true;
|
||||
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
|
||||
combine_holes(cache.holes);
|
||||
combine_shells(cache.bottom_surfaces);
|
||||
}
|
||||
|
||||
if (!at_least_one_bottom_projected && i >= 0) {
|
||||
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
|
||||
layerm->flow(frExternalPerimeter).scaled_spacing()),
|
||||
to_polygons(m_layers[i]->lslices));
|
||||
combine_shells(anchor_area);
|
||||
}
|
||||
|
||||
if (one_more_layer_below_top_bottom_surfaces)
|
||||
if (i >= 0 &&
|
||||
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
|
||||
combine_holes(cache_top_botom_regions[i].holes);
|
||||
}
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
|
||||
@@ -2919,8 +2831,11 @@ void PrintObject::discover_vertical_shells()
|
||||
Polygons object_volume;
|
||||
Polygons internal_volume;
|
||||
{
|
||||
if (idx_layer > 0 && idx_layer + 1 < m_layers.size())
|
||||
object_volume = to_polygons(intersection_ex_by_piece(m_layers[idx_layer - 1]->lslices, to_polygons(m_layers[idx_layer + 1]->lslices)));
|
||||
Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{};
|
||||
Polygons shrinked_upper_slice = (idx_layer + 1) < m_layers.size() ?
|
||||
to_polygons(m_layers[idx_layer + 1]->lslices) :
|
||||
Polygons{};
|
||||
object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice);
|
||||
internal_volume = closing(polygonsInternal, SCALED_EPSILON);
|
||||
}
|
||||
|
||||
@@ -2931,34 +2846,15 @@ void PrintObject::discover_vertical_shells()
|
||||
// the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy
|
||||
// 2. the area does not fully cover an internal polygon
|
||||
// This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small
|
||||
// Both tests below compare a small piece against the whole layer. Done literally, that is
|
||||
// quadratic in the number of pieces, which is what a layer split up by colour painting has,
|
||||
// so each is restricted to the part of the layer near the piece with an identical result:
|
||||
// object_volume is clipped to the piece's box, and only the internal polygons whose box meets
|
||||
// the expanded piece take part in the count, since the others pass through the difference
|
||||
// unchanged and add the same number to both sides of it.
|
||||
std::vector<BoundingBox> internal_bboxes;
|
||||
internal_bboxes.reserve(internal_volume.size());
|
||||
for (const Polygon &poly : internal_volume)
|
||||
internal_bboxes.emplace_back(get_extents(poly));
|
||||
regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(),
|
||||
[&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing,
|
||||
[&internal_volume, &min_perimeter_infill_spacing,
|
||||
&object_volume](const ExPolygon &p) {
|
||||
const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
|
||||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
|
||||
diff(to_polygons(p),
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
|
||||
object_volume, get_extents(p).inflated(SCALED_EPSILON)))
|
||||
.empty());
|
||||
if (!small)
|
||||
return false;
|
||||
const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing);
|
||||
const BoundingBox bbox = get_extents(expanded);
|
||||
Polygons nearby;
|
||||
for (size_t i = 0; i < internal_volume.size(); ++i)
|
||||
if (internal_bboxes[i].overlap(bbox))
|
||||
nearby.emplace_back(internal_volume[i]);
|
||||
return diff(nearby, expanded).size() >= nearby.size();
|
||||
return (p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
|
||||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
|
||||
diff(to_polygons(p), object_volume).empty())) &&
|
||||
diff(internal_volume,
|
||||
expand(to_polygons(p), min_perimeter_infill_spacing))
|
||||
.size() >= internal_volume.size();
|
||||
}),
|
||||
regularized_shell.end());
|
||||
}
|
||||
@@ -2980,9 +2876,8 @@ void PrintObject::discover_vertical_shells()
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
|
||||
// Trim the internal & internalvoid by the shell.
|
||||
const Polygons regularized_shell_polygons = to_polygons(regularized_shell);
|
||||
Slic3r::ExPolygons new_internal = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternal)), regularized_shell_polygons);
|
||||
Slic3r::ExPolygons new_internal_void = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternalVoid)), regularized_shell_polygons);
|
||||
Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell);
|
||||
Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), regularized_shell);
|
||||
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
{
|
||||
@@ -3009,15 +2904,7 @@ void PrintObject::discover_vertical_shells()
|
||||
layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final");
|
||||
}
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
}; // for each region
|
||||
if (top_bottom_surfaces_all_regions)
|
||||
// Nothing is shared between the regions, and a layer cut through a fine relief takes far longer than the others,
|
||||
// so they run next to each other instead of one after another.
|
||||
tbb::parallel_for(size_t(0), this->num_printing_regions(), process_region);
|
||||
else
|
||||
// Here every region fills the one top/bottom cache with its own surfaces first.
|
||||
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id)
|
||||
process_region(region_id);
|
||||
} // for each region
|
||||
} // void PrintObject::discover_vertical_shells()
|
||||
|
||||
// #define DEBUG_BRIDGE_OVER_INFILL
|
||||
@@ -3538,16 +3425,6 @@ void PrintObject::bridge_over_infill()
|
||||
vertical_lines[i].b = Point{x, y_max};
|
||||
}
|
||||
|
||||
// The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be
|
||||
// hit. Leaving them out gives the same intersections without building a tree over the whole layer's
|
||||
// boundary for every bridge.
|
||||
const coord_t scan_x_min = bb_x.min.x();
|
||||
const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing;
|
||||
anchors.erase(std::remove_if(anchors.begin(), anchors.end(),
|
||||
[scan_x_min, scan_x_max](const Line &l) {
|
||||
return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max;
|
||||
}),
|
||||
anchors.end());
|
||||
auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)};
|
||||
auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)};
|
||||
|
||||
@@ -3792,58 +3669,26 @@ void PrintObject::bridge_over_infill()
|
||||
|
||||
std::vector<CandidateSurface> expanded_surfaces;
|
||||
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
|
||||
// The expanded fill boundary depends only on the bridging flow, and total_fill_area is not
|
||||
// modified below, so build it once per spacing rather than once per candidate. A layer split
|
||||
// into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each.
|
||||
std::map<coord_t, Polylines> boundary_by_spacing;
|
||||
// expansion_area is a clean, non-overlapping set, so cutting a bridge out of it only changes the
|
||||
// polygons near that bridge. The rest are passed through untouched instead of being fed to Clipper
|
||||
// with the whole layer again for every candidate.
|
||||
// Not `near`/`far`: the Windows headers still define those as macros, and they expand to
|
||||
// nothing, which turns the declaration below into an empty one.
|
||||
const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &rest) {
|
||||
Polygons nearby;
|
||||
for (const Polygon &p : polys)
|
||||
(get_extents(p).overlap(bbox) ? nearby : rest).emplace_back(p);
|
||||
return nearby;
|
||||
};
|
||||
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
|
||||
region_config.sparse_infill_pattern == ipOctagramSpiral;
|
||||
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
|
||||
const Polygons expanded_polys = expand(candidate.new_polys, flow.scaled_spacing());
|
||||
// deep_infill_area and internal_unsupported_area cover the whole layer; only their part under
|
||||
// this candidate can change the results, so they are clipped to its box first.
|
||||
ExPolygons bridge_components;
|
||||
if (!expanded_polys.empty())
|
||||
bridge_components = intersection_ex(expanded_polys,
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
|
||||
deep_infill_area, get_extents(expanded_polys).inflated(SCALED_EPSILON)));
|
||||
ExPolygons bridge_components = intersection_ex(expand(candidate.new_polys, flow.scaled_spacing()), deep_infill_area);
|
||||
// Orca: Filter whole bridge areas so their holes remain holes.
|
||||
bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(),
|
||||
[&internal_unsupported_area](const ExPolygon &component) {
|
||||
return intersection_ex(component, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
|
||||
internal_unsupported_area,
|
||||
get_extents(component).inflated(SCALED_EPSILON)))
|
||||
.empty();
|
||||
return intersection_ex(component, internal_unsupported_area).empty();
|
||||
}),
|
||||
bridge_components.end());
|
||||
Polygons area_to_be_bridge = to_polygons(std::move(bridge_components));
|
||||
|
||||
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
|
||||
|
||||
if (area_to_be_bridge.empty())
|
||||
continue;
|
||||
|
||||
// Not split like the cut of expansion_area below: the whole limiting area is grown by 30% of the spacing,
|
||||
// which merges neighbouring polygons, and any of its boundary can anchor the bridge.
|
||||
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
|
||||
|
||||
auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing());
|
||||
if (boundary_it == boundary_by_spacing.end())
|
||||
boundary_it = boundary_by_spacing
|
||||
.emplace(flow.scaled_spacing(), to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing())))
|
||||
.first;
|
||||
Polylines boundary_plines = boundary_it->second;
|
||||
Polylines boundary_plines = to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing()));
|
||||
{
|
||||
Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3f * flow.scaled_spacing()));
|
||||
boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end());
|
||||
@@ -3919,12 +3764,9 @@ void PrintObject::bridge_over_infill()
|
||||
// Check collision with other expanded surfaces
|
||||
{
|
||||
bool reconstruct = false;
|
||||
Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing());
|
||||
const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area);
|
||||
Polygons tmp_expanded_area = expand(bridging_area, 3.0f * flow.scaled_spacing());
|
||||
for (const CandidateSurface &s : expanded_surfaces) {
|
||||
// Surfaces whose boxes miss each other cannot intersect, which is most pairs on a busy layer.
|
||||
if (get_extents(s.new_polys).overlap(tmp_expanded_bbox) &&
|
||||
!intersection(s.new_polys, tmp_expanded_area).empty()) {
|
||||
if (!intersection(s.new_polys, tmp_expanded_area).empty()) {
|
||||
bridging_angle = s.bridge_angle;
|
||||
reconstruct = true;
|
||||
break;
|
||||
@@ -3948,20 +3790,10 @@ void PrintObject::bridge_over_infill()
|
||||
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
|
||||
bridging_angle, scan_spacing, true));
|
||||
}
|
||||
// Each of these meets one bridge with the whole layer, so the layer side is first cut down to the
|
||||
// bridge's box (and expansion_area split as above); the result is the same.
|
||||
if (!bridging_area.empty()) {
|
||||
const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON);
|
||||
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox));
|
||||
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox));
|
||||
bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox));
|
||||
}
|
||||
if (!bridging_area.empty()) {
|
||||
Polygons kept;
|
||||
const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept);
|
||||
append(kept, diff(cut, bridging_area));
|
||||
expansion_area = std::move(kept);
|
||||
}
|
||||
bridging_area = intersection(bridging_area, limiting_area);
|
||||
bridging_area = intersection(bridging_area, total_fill_area);
|
||||
bridging_area = diff(bridging_area, total_top_area);
|
||||
expansion_area = diff(expansion_area, bridging_area);
|
||||
|
||||
#ifdef DEBUG_BRIDGE_OVER_INFILL
|
||||
debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r),
|
||||
|
||||
+484
-11
@@ -26,8 +26,10 @@
|
||||
#include "Print.hpp"
|
||||
#include "GCode/OrderingStrategies.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <deque>
|
||||
#include "MultiMaterialSegmentation.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -96,6 +98,46 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
|
||||
return out;
|
||||
}
|
||||
|
||||
// The shortest chain over every order and allowed reversal, by dynamic programming over the subsets of segments, so only
|
||||
// for a few of them. end_points holds the first and the last point of each segment; the chain starts at start when given
|
||||
// and its end is free. Return index and "reversed" flag.
|
||||
static std::vector<std::pair<size_t, bool>> chain_segments_exact(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d *start)
|
||||
{
|
||||
const size_t n = could_reverse.size();
|
||||
assert(n >= 1 && n <= 12 && end_points.size() == 2 * n);
|
||||
// State (mask * n + j) * 2 + o: the segments of mask visited, the last one j, flipped when o.
|
||||
const size_t num_states = (size_t(1) << n) * n * 2;
|
||||
std::vector<double> cost(num_states, std::numeric_limits<double>::max());
|
||||
std::vector<size_t> from(num_states, size_t(-1));
|
||||
auto entry = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + o]; };
|
||||
auto exit = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + 1 - o]; };
|
||||
for (size_t j = 0; j < n; ++ j)
|
||||
for (size_t o = 0; o <= size_t(could_reverse[j] != 0); ++ o)
|
||||
cost[((size_t(1) << j) * n + j) * 2 + o] = start == nullptr ? 0. : (entry(j, o) - *start).norm();
|
||||
for (size_t mask = 1; mask < (size_t(1) << n); ++ mask)
|
||||
for (size_t s = mask * n * 2; s < (mask + 1) * n * 2; ++ s)
|
||||
if (cost[s] < std::numeric_limits<double>::max())
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
if (! (mask >> k & 1))
|
||||
for (size_t o = 0; o <= size_t(could_reverse[k] != 0); ++ o) {
|
||||
const size_t next = (((mask | size_t(1) << k) * n) + k) * 2 + o;
|
||||
const double c = cost[s] + (entry(k, o) - exit(s / 2 % n, s & 1)).norm();
|
||||
if (c < cost[next]) {
|
||||
cost[next] = c;
|
||||
from[next] = s;
|
||||
}
|
||||
}
|
||||
const size_t full = (size_t(1) << n) - 1;
|
||||
size_t best = full * n * 2;
|
||||
for (size_t s = best; s < num_states; ++ s)
|
||||
if (cost[s] < cost[best])
|
||||
best = s;
|
||||
std::vector<std::pair<size_t, bool>> out(n);
|
||||
for (size_t s = best, k = n; k -- > 0; s = from[s])
|
||||
out[k] = { s / 2 % n, (s & 1) != 0 };
|
||||
return out;
|
||||
}
|
||||
|
||||
// Chain perimeters (always closed) and thin fills (closed or open) using a greedy algorithm.
|
||||
// Solving a Traveling Salesman Problem (TSP) with the modification, that the sites are not always points, but points and segments.
|
||||
// Solving using a greedy algorithm, where a shortest edge is added to the solution if it does not produce a bifurcation or a cycle.
|
||||
@@ -104,6 +146,7 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
|
||||
// The algorithm builds a tour for the traveling salesman one edge at a time and thus maintains multiple tour fragments, each of which
|
||||
// is a simple path in the complete graph of cities. At each stage, the algorithm selects the edge of minimal cost that either creates
|
||||
// a new fragment, extends one of the existing paths or creates a cycle of length equal to the number of cities.
|
||||
// Orca: two to four segments are chained exactly instead.
|
||||
template<typename PointType, typename SegmentEndPointFunc, bool REVERSE_COULD_FAIL, typename CouldReverseFunc>
|
||||
std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals_(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near)
|
||||
{
|
||||
@@ -118,6 +161,19 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals
|
||||
out.emplace_back(0, could_reverse_func(0) && start_near != nullptr &&
|
||||
(end_point_func(0, false) - *start_near).template cast<double>().squaredNorm() < (end_point_func(0, true) - *start_near).template cast<double>().squaredNorm());
|
||||
}
|
||||
else if (num_segments <= 4)
|
||||
{
|
||||
// Orca: so few segments are cheap to chain exactly. The first one is then not always the one nearest start_near.
|
||||
std::vector<Vec2d> end_points(2 * num_segments);
|
||||
std::vector<char> could_reverse(num_segments);
|
||||
for (size_t i = 0; i < num_segments; ++ i) {
|
||||
end_points[2 * i] = end_point_func(i, true).template cast<double>();
|
||||
end_points[2 * i + 1] = end_point_func(i, false).template cast<double>();
|
||||
could_reverse[i] = could_reverse_func(i);
|
||||
}
|
||||
const Vec2d start = start_near != nullptr ? Vec2d(start_near->template cast<double>()) : Vec2d::Zero();
|
||||
out = chain_segments_exact(end_points, could_reverse, start_near != nullptr ? &start : nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// End points of segments for the KD tree closest point search.
|
||||
@@ -1039,12 +1095,370 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy2(SegmentEndPointFunc
|
||||
return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near);
|
||||
}
|
||||
|
||||
// The K points nearest to a query, nearest first, ties by index, from a uniform grid of about two points per cell.
|
||||
template<size_t K>
|
||||
class NearestPoints
|
||||
{
|
||||
public:
|
||||
static constexpr size_t npos = size_t(-1);
|
||||
|
||||
explicit NearestPoints(const std::vector<Vec2d> &points) : m_points(points)
|
||||
{
|
||||
BoundingBoxf bbox;
|
||||
for (const Vec2d &p : points)
|
||||
bbox.merge(p);
|
||||
const Vec2d size = bbox.size();
|
||||
const double count = double(std::max<size_t>(points.size(), 1));
|
||||
m_cell = std::max(std::sqrt(2. * size.x() * size.y() / count), std::max(size.x(), size.y()) / count);
|
||||
if (m_cell <= 0.)
|
||||
m_cell = 1.;
|
||||
m_origin = bbox.min;
|
||||
m_cols = int(size.x() / m_cell) + 1;
|
||||
m_rows = int(size.y() / m_cell) + 1;
|
||||
m_first.assign(size_t(m_cols) * m_rows + 1, 0);
|
||||
for (const Vec2d &p : points)
|
||||
++ m_first[cell(p) + 1];
|
||||
for (size_t c = 1; c < m_first.size(); ++ c)
|
||||
m_first[c] += m_first[c - 1];
|
||||
m_index.resize(points.size());
|
||||
std::vector<size_t> next(m_first.begin(), m_first.end() - 1);
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
m_index[next[cell(points[i])] ++] = i;
|
||||
}
|
||||
|
||||
// Indices of the nearest points that filter accepts, npos past the last one.
|
||||
template<typename Filter>
|
||||
std::array<size_t, K> query(const Vec2d &q, Filter filter) const
|
||||
{
|
||||
std::array<std::pair<double, size_t>, K> best;
|
||||
size_t found = 0;
|
||||
const int cx = int(std::clamp(std::floor((q.x() - m_origin.x()) / m_cell), 0., double(m_cols - 1)));
|
||||
const int cy = int(std::clamp(std::floor((q.y() - m_origin.y()) / m_cell), 0., double(m_rows - 1)));
|
||||
auto visit = [&](int x, int y) {
|
||||
if (x < 0 || y < 0 || x >= m_cols || y >= m_rows)
|
||||
return;
|
||||
const size_t c = size_t(y) * m_cols + x;
|
||||
for (size_t k = m_first[c]; k < m_first[c + 1]; ++ k) {
|
||||
const size_t i = m_index[k];
|
||||
if (! filter(i))
|
||||
continue;
|
||||
const std::pair<double, size_t> candidate((m_points[i] - q).squaredNorm(), i);
|
||||
if (found == K && ! (candidate < best[K - 1]))
|
||||
continue;
|
||||
auto it = std::upper_bound(best.begin(), best.begin() + found, candidate);
|
||||
std::move_backward(it, best.begin() + std::min(found, K - 1), best.begin() + std::min(found + 1, K));
|
||||
*it = candidate;
|
||||
found = std::min(found + 1, K);
|
||||
}
|
||||
};
|
||||
// Points beyond ring r are at least r cells away.
|
||||
for (int r = 0; r <= std::max(m_cols, m_rows); ++ r) {
|
||||
if (r == 0)
|
||||
visit(cx, cy);
|
||||
for (int d = -r; d <= r && r > 0; ++ d) {
|
||||
visit(cx + d, cy - r);
|
||||
visit(cx + d, cy + r);
|
||||
if (d != -r && d != r) {
|
||||
visit(cx - r, cy + d);
|
||||
visit(cx + r, cy + d);
|
||||
}
|
||||
}
|
||||
if (found == K && best[K - 1].first <= sqr(r * m_cell))
|
||||
break;
|
||||
}
|
||||
std::array<size_t, K> out;
|
||||
out.fill(npos);
|
||||
for (size_t k = 0; k < found; ++ k)
|
||||
out[k] = best[k].second;
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t cell(const Vec2d &p) const
|
||||
{
|
||||
const int x = std::clamp(int((p.x() - m_origin.x()) / m_cell), 0, m_cols - 1);
|
||||
const int y = std::clamp(int((p.y() - m_origin.y()) / m_cell), 0, m_rows - 1);
|
||||
return size_t(y) * m_cols + x;
|
||||
}
|
||||
|
||||
const std::vector<Vec2d> &m_points;
|
||||
Vec2d m_origin;
|
||||
double m_cell;
|
||||
int m_cols;
|
||||
int m_rows;
|
||||
std::vector<size_t> m_first;
|
||||
std::vector<size_t> m_index;
|
||||
};
|
||||
|
||||
// Shortens the travel of a chain with 2-opt moves (reversing a run of segments) and or-opt moves (moving a run of up to
|
||||
// three segments elsewhere, or a longer one to the front, flipped or not). end_points holds the first and the last point
|
||||
// of each segment. The chain starts at start_near when given, and its end is free. A move that would flip a segment that
|
||||
// cannot reverse is skipped. Moves only create links between nearest neighbor end points, and after a move only the
|
||||
// segments next to a changed link are examined again, until a sweep of all segments finds no move. This keeps the search
|
||||
// close to linear.
|
||||
static void improve_chain_by_local_search(std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points,
|
||||
const std::vector<char> &could_reverse, const Vec2d *start_near)
|
||||
{
|
||||
const size_t n = chain.size();
|
||||
if (n < 2)
|
||||
return;
|
||||
assert(end_points.size() == 2 * n && could_reverse.size() == n);
|
||||
|
||||
std::vector<size_t> order(n), pos(n), fixed_before(n + 1, 0);
|
||||
std::vector<char> flipped(n);
|
||||
for (size_t k = 0; k < n; ++ k) {
|
||||
order[k] = chain[k].first;
|
||||
flipped[order[k]] = chain[k].second;
|
||||
}
|
||||
// Updates positions lo..hi after a move, which keeps the number of segments that cannot reverse past hi.
|
||||
auto refresh = [&](size_t lo, size_t hi) {
|
||||
for (size_t k = lo; k <= hi; ++ k) {
|
||||
pos[order[k]] = k;
|
||||
fixed_before[k + 1] = fixed_before[k] + ! could_reverse[order[k]];
|
||||
}
|
||||
};
|
||||
refresh(0, n - 1);
|
||||
auto entry_idx = [&](size_t k) { return 2 * order[k] + flipped[order[k]]; };
|
||||
auto exit_idx = [&](size_t k) { return 2 * order[k] + 1 - flipped[order[k]]; };
|
||||
auto entry = [&](size_t k) -> const Vec2d& { return end_points[entry_idx(k)]; };
|
||||
auto exit = [&](size_t k) -> const Vec2d& { return end_points[exit_idx(k)]; };
|
||||
auto is_exit = [&](size_t e) { return char(e & 1) != flipped[e >> 1]; };
|
||||
auto reversible = [&](size_t i, size_t j) { return fixed_before[j + 1] == fixed_before[i]; };
|
||||
auto dist = [](const Vec2d *a, const Vec2d *b) { return a == nullptr || b == nullptr ? 0. : (*a - *b).norm(); };
|
||||
// Travel end points around position k, null where the chain is open.
|
||||
auto prev_exit = [&](size_t k) -> const Vec2d* { return k > 0 ? &exit(k - 1) : start_near; };
|
||||
auto next_entry = [&](size_t k) -> const Vec2d* { return k + 1 < n ? &entry(k + 1) : nullptr; };
|
||||
|
||||
// The 32 nearest end points are the candidates, which covers every move on short chains; 8 missed much of the gain.
|
||||
static constexpr size_t K = 32;
|
||||
static constexpr size_t npos = NearestPoints<K>::npos;
|
||||
const NearestPoints<K> nearest(end_points);
|
||||
std::vector<std::array<size_t, K>> neighbors(end_points.size());
|
||||
for (size_t e = 0; e < end_points.size(); ++ e)
|
||||
neighbors[e] = nearest.query(end_points[e], [e](size_t idx) { return (idx >> 1) != (e >> 1); });
|
||||
std::array<size_t, K> start_neighbors;
|
||||
start_neighbors.fill(npos);
|
||||
if (start_near != nullptr)
|
||||
start_neighbors = nearest.query(*start_near, [](size_t) { return true; });
|
||||
|
||||
// Segments to examine, by id, each queued once.
|
||||
std::deque<size_t> queue;
|
||||
std::vector<char> queued(n, 0);
|
||||
auto enqueue = [&](size_t k) {
|
||||
if (k < n && ! queued[order[k]]) {
|
||||
queued[order[k]] = 1;
|
||||
queue.push_back(order[k]);
|
||||
}
|
||||
};
|
||||
|
||||
const double min_gain = SCALED_EPSILON;
|
||||
// Reverses positions i..j if that shortens the chain.
|
||||
auto try_2opt = [&](size_t i, size_t j) {
|
||||
if (! reversible(i, j))
|
||||
return false;
|
||||
const Vec2d *a = prev_exit(i), *b = next_entry(j);
|
||||
if (dist(a, &exit(j)) + dist(&entry(i), b) > dist(a, &entry(i)) + dist(&exit(j), b) - min_gain)
|
||||
return false;
|
||||
std::reverse(order.begin() + i, order.begin() + j + 1);
|
||||
for (size_t k = i; k <= j; ++ k)
|
||||
flipped[order[k]] ^= 1;
|
||||
refresh(i, j);
|
||||
for (size_t k : { i - 1, i, j, j + 1 })
|
||||
enqueue(k);
|
||||
return true;
|
||||
};
|
||||
// Moves positions i..last after position gap (size_t(-1) for the front) if that shortens the chain.
|
||||
auto try_oropt = [&](size_t i, size_t last, size_t gap) {
|
||||
if (gap + 1 == i || (gap >= i && gap <= last))
|
||||
return false;
|
||||
const Vec2d *a = gap == size_t(-1) ? start_near : &exit(gap);
|
||||
const Vec2d *b = gap + 1 < n ? &entry(gap + 1) : nullptr;
|
||||
const double removal_gain = dist(prev_exit(i), &entry(i)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(i), next_entry(last));
|
||||
const double base = dist(a, b);
|
||||
const double cost_kept = dist(a, &entry(i)) + dist(&exit(last), b) - base;
|
||||
const double cost_flipped = reversible(i, last) ? dist(a, &exit(last)) + dist(&entry(i), b) - base : std::numeric_limits<double>::max();
|
||||
if (std::min(cost_kept, cost_flipped) > removal_gain - min_gain)
|
||||
return false;
|
||||
const size_t len = last - i + 1;
|
||||
// The run lands on first..first+len-1; lo..hi spans every position that changed.
|
||||
const bool forward = gap > last && gap != size_t(-1);
|
||||
const size_t first = forward ? gap + 1 - len : gap + 1;
|
||||
const size_t lo = forward ? i : first;
|
||||
const size_t hi = forward ? gap : last;
|
||||
if (forward)
|
||||
std::rotate(order.begin() + i, order.begin() + last + 1, order.begin() + gap + 1);
|
||||
else
|
||||
std::rotate(order.begin() + first, order.begin() + i, order.begin() + last + 1);
|
||||
if (cost_flipped < cost_kept) {
|
||||
std::reverse(order.begin() + first, order.begin() + first + len);
|
||||
for (size_t k = first; k < first + len; ++ k)
|
||||
flipped[order[k]] ^= 1;
|
||||
}
|
||||
refresh(lo, hi);
|
||||
// The links around the run's new place, and the link closing its old place.
|
||||
for (size_t k : { first - 1, first, first + len - 1, first + len, forward ? i - 1 : last, forward ? i : last + 1 })
|
||||
enqueue(k);
|
||||
return true;
|
||||
};
|
||||
// The gap before position k of the chain with positions i..last taken out.
|
||||
auto gap_before = [](size_t k, size_t i, size_t last) { return k == last + 1 ? i - 1 : k - 1; };
|
||||
|
||||
// Applies the first move found that breaks a link next to position p.
|
||||
// Candidates are tried nearest first, while closer than the link the move would replace at that end point.
|
||||
auto closer = [&](const Vec2d &at, size_t q, double limit) { return q != npos && (end_points[q] - at).norm() < limit; };
|
||||
auto improve_at = [&](size_t p) {
|
||||
// 2-opt breaking the link into p, as the first or the last link of the reversed run.
|
||||
if (const Vec2d *a = prev_exit(p); a != nullptr) {
|
||||
const double old_link = dist(a, &entry(p));
|
||||
for (size_t q : p > 0 ? neighbors[exit_idx(p - 1)] : start_neighbors) {
|
||||
if (! closer(*a, q, old_link))
|
||||
break;
|
||||
if (is_exit(q) && pos[q >> 1] >= p && try_2opt(p, pos[q >> 1]))
|
||||
return true;
|
||||
}
|
||||
if (p > 0)
|
||||
for (size_t q : neighbors[entry_idx(p)]) {
|
||||
if (! closer(entry(p), q, old_link))
|
||||
break;
|
||||
if (! is_exit(q) && pos[q >> 1] < p && try_2opt(pos[q >> 1], p - 1))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 2-opt breaking the link out of p.
|
||||
if (p + 1 < n) {
|
||||
const double old_link = dist(&exit(p), &entry(p + 1));
|
||||
for (size_t q : neighbors[exit_idx(p)]) {
|
||||
if (! closer(exit(p), q, old_link))
|
||||
break;
|
||||
if (is_exit(q) && pos[q >> 1] > p && try_2opt(p + 1, pos[q >> 1]))
|
||||
return true;
|
||||
}
|
||||
for (size_t q : neighbors[entry_idx(p + 1)]) {
|
||||
if (! closer(entry(p + 1), q, old_link))
|
||||
break;
|
||||
if (! is_exit(q) && pos[q >> 1] <= p && try_2opt(pos[q >> 1], p))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// A run of any length that starts or ends next to the start point moves to the front.
|
||||
if (p == 0)
|
||||
for (size_t q : start_neighbors) {
|
||||
if (q == npos)
|
||||
break;
|
||||
const size_t m = pos[q >> 1];
|
||||
if (m == 0)
|
||||
continue;
|
||||
// Runs m..k enter next to the start, runs k..m leave there and go flipped.
|
||||
const bool exit_near = is_exit(q);
|
||||
for (size_t k = exit_near ? 1 : m; k < (exit_near ? m + 1 : n); ++ k)
|
||||
if (try_oropt(exit_near ? k : m, exit_near ? m : k, size_t(-1)))
|
||||
return true;
|
||||
}
|
||||
// Or-opt of the runs starting at p: next to an exit a run goes after that segment, next to an entry before it.
|
||||
for (size_t last = p; last < std::min(p + 3, n); ++ last) {
|
||||
if (try_oropt(p, last, size_t(-1)))
|
||||
return true;
|
||||
const double removal_gain = dist(prev_exit(p), &entry(p)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(p), next_entry(last));
|
||||
for (size_t e : { entry_idx(p), exit_idx(last) })
|
||||
for (size_t q : neighbors[e]) {
|
||||
if (! closer(end_points[e], q, removal_gain))
|
||||
break;
|
||||
const size_t m = pos[q >> 1];
|
||||
if ((m < p || m > last) && try_oropt(p, last, is_exit(q) ? m : gap_before(m, p, last)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// A new link can make room for a run that sits elsewhere, hence the sweeps until one finds no move. Every move shortens
|
||||
// the chain, so the search ends; the cap only bounds its time.
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
enqueue(k);
|
||||
bool moved_since_sweep = false;
|
||||
for (size_t moves = 0; moves < 50 * n;) {
|
||||
if (queue.empty()) {
|
||||
if (! moved_since_sweep)
|
||||
break;
|
||||
moved_since_sweep = false;
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
enqueue(k);
|
||||
}
|
||||
const size_t segment = queue.front();
|
||||
queue.pop_front();
|
||||
queued[segment] = 0;
|
||||
if (improve_at(pos[segment])) {
|
||||
++ moves;
|
||||
moved_since_sweep = true;
|
||||
enqueue(pos[segment]);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
chain[k] = { order[k], flipped[order[k]] != 0 };
|
||||
}
|
||||
|
||||
// Chain that always goes on to the nearest free end point, from start.
|
||||
static std::vector<std::pair<size_t, bool>> chain_nearest_next(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d &start)
|
||||
{
|
||||
const NearestPoints<1> nearest(end_points);
|
||||
std::vector<char> taken(could_reverse.size(), 0);
|
||||
std::vector<std::pair<size_t, bool>> out;
|
||||
out.reserve(could_reverse.size());
|
||||
for (Vec2d at = start; out.size() < could_reverse.size();) {
|
||||
const size_t e = nearest.query(at, [&](size_t idx) { return ! taken[idx >> 1] && ((idx & 1) == 0 || could_reverse[idx >> 1]); })[0];
|
||||
taken[e >> 1] = 1;
|
||||
out.emplace_back(e >> 1, (e & 1) != 0);
|
||||
at = end_points[e ^ 1];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static double chain_travel(const std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points, const Vec2d &start)
|
||||
{
|
||||
double travel = 0.;
|
||||
Vec2d at = start;
|
||||
for (const std::pair<size_t, bool> &segment : chain) {
|
||||
travel += (end_points[2 * segment.first + segment.second] - at).norm();
|
||||
at = end_points[2 * segment.first + 1 - segment.second];
|
||||
}
|
||||
return travel;
|
||||
}
|
||||
|
||||
template<typename EntityPtr>
|
||||
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near)
|
||||
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near, bool improve = false)
|
||||
{
|
||||
auto segment_end_point = [&entities](size_t idx, bool first_point) -> Point { return first_point ? entities[idx]->first_point() : entities[idx]->last_point(); };
|
||||
auto could_reverse = [&entities](size_t idx) { const ExtrusionEntity *ee = entities[idx]; return ee->is_loop() || ee->can_reverse(); };
|
||||
std::vector<std::pair<size_t, bool>> out = chain_segments_greedy_constrained_reversals<Point, decltype(segment_end_point), decltype(could_reverse)>(segment_end_point, could_reverse, entities.size(), start_near);
|
||||
if (improve && out.size() > 1) {
|
||||
std::vector<Vec2d> end_points;
|
||||
std::vector<char> reversible;
|
||||
end_points.reserve(2 * entities.size());
|
||||
reversible.reserve(entities.size());
|
||||
for (size_t idx = 0; idx < entities.size(); ++ idx) {
|
||||
end_points.emplace_back(segment_end_point(idx, true).template cast<double>());
|
||||
end_points.emplace_back(segment_end_point(idx, false).template cast<double>());
|
||||
reversible.emplace_back(could_reverse(idx));
|
||||
}
|
||||
Vec2d start = Vec2d::Zero();
|
||||
if (start_near != nullptr)
|
||||
start = start_near->cast<double>();
|
||||
if (entities.size() <= 8) {
|
||||
// Few paths are chained exactly; the local search below can stop short of the shortest chain.
|
||||
out = chain_segments_exact(end_points, reversible, start_near != nullptr ? &start : nullptr);
|
||||
} else {
|
||||
improve_chain_by_local_search(out, end_points, reversible, start_near != nullptr ? &start : nullptr);
|
||||
if (start_near != nullptr) {
|
||||
// The greedy chain can settle into a sweep that local moves cannot undo, which a nearest-next chain may avoid.
|
||||
std::vector<std::pair<size_t, bool>> nearest_next = chain_nearest_next(end_points, reversible, start);
|
||||
if (chain_travel(nearest_next, end_points, start) < chain_travel(out, end_points, start) - SCALED_EPSILON) {
|
||||
improve_chain_by_local_search(nearest_next, end_points, reversible, &start);
|
||||
out = std::move(nearest_next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::pair<size_t, bool> &segment : out) {
|
||||
const ExtrusionEntity *ee = entities[segment.first];
|
||||
if (ee->is_loop())
|
||||
@@ -1086,7 +1500,8 @@ void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const s
|
||||
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near)
|
||||
{
|
||||
chain_and_reorder_extrusion_entities(entities, &start_near);
|
||||
remove_entities_without_endpoints(entities);
|
||||
reorder_extrusion_entities(entities, chain_extrusion_entities_impl(entities, &start_near, true));
|
||||
}
|
||||
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near)
|
||||
@@ -1101,7 +1516,7 @@ void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &e
|
||||
remove_entities_without_endpoints(entities);
|
||||
std::vector<const ExtrusionEntity*> out;
|
||||
out.reserve(entities.size());
|
||||
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near)) {
|
||||
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near, true)) {
|
||||
if (reverse) {
|
||||
ExtrusionEntity *clone = reversed_clones.emplace_back(entities[idx]->clone()).get();
|
||||
clone->reverse();
|
||||
@@ -1159,16 +1574,74 @@ std::vector<size_t> chain_points(const Points &points, const Point *start_near)
|
||||
return out;
|
||||
}
|
||||
|
||||
// Moves runs of up to three points of the cycle next to one of the 8 points nearest to an end of the run, reversed or not,
|
||||
// while that shortens the cycle. path[0] stays first. Returns true if any move was made.
|
||||
static bool improve_cycle_by_or_opt(std::vector<size_t> &path, const Points &points)
|
||||
{
|
||||
const size_t pn = path.size();
|
||||
if (pn < 5)
|
||||
return false;
|
||||
std::vector<Vec2d> coords(points.size());
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
coords[i] = points[i].cast<double>();
|
||||
const NearestPoints<8> grid(coords);
|
||||
std::vector<size_t> pos(points.size());
|
||||
auto refresh = [&]() { for (size_t k = 0; k < pn; ++ k) pos[path[k]] = k; };
|
||||
refresh();
|
||||
auto dist = [&](size_t a, size_t b) { return (coords[path[a % pn]] - coords[path[b % pn]]).norm(); };
|
||||
bool improved = false;
|
||||
for (bool moved = true; moved;) {
|
||||
moved = false;
|
||||
for (size_t len = 1; len <= 3; ++ len)
|
||||
for (size_t i = 1; i + len <= pn; ++ i) {
|
||||
// The run path[i..last] goes between path[k] and path[k + 1].
|
||||
const size_t last = i + len - 1;
|
||||
const double removal = dist(i - 1, i) + dist(last, last + 1) - dist(i - 1, last + 1);
|
||||
bool applied = removal <= SCALED_EPSILON;
|
||||
for (size_t end : { path[i], path[last] })
|
||||
for (size_t q : grid.query(coords[end], [end](size_t idx) { return idx != end; }))
|
||||
for (size_t k : { q == NearestPoints<8>::npos ? pn : (pos[q] + pn - 1) % pn, q == NearestPoints<8>::npos ? pn : pos[q] }) {
|
||||
if (applied || k == pn || (k + 1 >= i && k <= last))
|
||||
continue;
|
||||
const double base = dist(k, k + 1);
|
||||
const double kept = dist(k, i) + dist(last, k + 1) - base;
|
||||
const double reversed = dist(k, last) + dist(i, k + 1) - base;
|
||||
if (std::min(kept, reversed) > removal - SCALED_EPSILON)
|
||||
continue;
|
||||
std::vector<size_t> run(path.begin() + i, path.begin() + last + 1);
|
||||
if (reversed < kept)
|
||||
std::reverse(run.begin(), run.end());
|
||||
path.erase(path.begin() + i, path.begin() + last + 1);
|
||||
path.insert(path.begin() + (k < i ? k + 1 : k + 1 - len), run.begin(), run.end());
|
||||
refresh();
|
||||
applied = moved = improved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return improved;
|
||||
}
|
||||
|
||||
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
|
||||
{
|
||||
std::vector<size_t> path = chain_points(points, start_near);
|
||||
// Alternate 2-opt and crossing removal until convergence.
|
||||
// 2-opt can create new crossings, and crossing removal can create new
|
||||
// opportunities for 2-opt improvement. Break early if neither improves.
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
bool improved = tsp_2opt_improve(path, points);
|
||||
improved |= tsp_remove_crossings(path, points);
|
||||
if (!improved) break;
|
||||
std::vector<size_t> path;
|
||||
if (points.size() <= 10) {
|
||||
// Orca: few points are toured exactly, from the point nearest start_near toward its nearer neighbor.
|
||||
path = tsp_exact_cycle(points);
|
||||
if (start_near != nullptr && path.size() > 2) {
|
||||
auto dist = [&points](size_t a, const Point &b) { return (points[a] - b).cast<double>().squaredNorm(); };
|
||||
std::rotate(path.begin(), std::min_element(path.begin(), path.end(), [&](size_t a, size_t b) { return dist(a, *start_near) < dist(b, *start_near); }), path.end());
|
||||
if (dist(path.back(), points[path.front()]) < dist(path[1], points[path.front()]))
|
||||
std::reverse(path.begin() + 1, path.end());
|
||||
}
|
||||
} else {
|
||||
path = chain_points(points, start_near);
|
||||
// Alternate 2-opt, or-opt and crossing removal: each can open moves for the others. Break early if none improves.
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
bool improved = tsp_2opt_improve(path, points);
|
||||
improved |= improve_cycle_by_or_opt(path, points);
|
||||
improved |= tsp_remove_crossings(path, points);
|
||||
if (!improved) break;
|
||||
}
|
||||
}
|
||||
if (start_near == nullptr)
|
||||
tsp_rotate_minimize_closing(path, points);
|
||||
|
||||
@@ -23,9 +23,12 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
|
||||
|
||||
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
|
||||
void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const std::vector<std::pair<size_t, bool>> &chain);
|
||||
// From start_near, the greedy chain is refined by 2-opt and or-opt moves, which shortens the travel at some extra cost.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near);
|
||||
// The greedy chain only.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
|
||||
// Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the originals stay unchanged.
|
||||
// Refined as above. Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the
|
||||
// originals stay unchanged.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &entities, const Point &start_near,
|
||||
std::vector<std::unique_ptr<ExtrusionEntity>> &reversed_clones);
|
||||
|
||||
|
||||
@@ -1004,9 +1004,9 @@ public:
|
||||
::fread(&y, sizeof(coord_t), 1, file);
|
||||
poly.points.emplace_back(Point(x * scale, y * scale));
|
||||
}
|
||||
printf("Polygon %d, area: %lf\n", i, area(poly.points));
|
||||
if (which == -1 || which == i)
|
||||
m_support_polygons_deserialized.emplace_back(std::move(poly));
|
||||
printf("Polygon %d, area: %lf\n", i, area(poly.points));
|
||||
}
|
||||
::fread(&n_polygons, 4, 1, file);
|
||||
m_trimming_polygons_deserialized.reserve(n_polygons);
|
||||
|
||||
@@ -996,46 +996,16 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
|
||||
if (is_auto(stype) && config_detect_sharp_tails)
|
||||
{
|
||||
// BBS detect sharp tail
|
||||
// On a belt, "below" has to include the belt itself and the
|
||||
// shear-advanced lower layer, or every belt-contact island reads as
|
||||
// a sharp tail -- which is what the empty-predecessor skip above was
|
||||
// really masking. effective_lower is exactly that notion of below.
|
||||
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
|
||||
// Each island is tested only against the lower islands whose box meets its own; overlaps() tries
|
||||
// every pair, which is quadratic in the island counts of the two layers.
|
||||
std::vector<BoundingBox> lower_bboxes;
|
||||
lower_bboxes.reserve(tail_lower.size());
|
||||
for (const ExPolygon &lower : tail_lower)
|
||||
lower_bboxes.emplace_back(get_extents(lower));
|
||||
for (const ExPolygon& expoly : curr_polys) {
|
||||
bool is_sharp_tail = false;
|
||||
// 1. nothing below
|
||||
// this is a sharp tail region if it's floating and non-ignorable
|
||||
const ExPolygons expanded = offset_ex(expoly, 0.1 * extrusion_width_scaled);
|
||||
const BoundingBox bbox = get_extents(expanded);
|
||||
ExPolygons lower_nearby;
|
||||
for (size_t i = 0; i < tail_lower.size(); ++i)
|
||||
if (lower_bboxes[i].overlap(bbox))
|
||||
lower_nearby.emplace_back(tail_lower[i]);
|
||||
// As overlaps(expanded, lower_nearby), with each lower island cut to the island's box first:
|
||||
// below a fine relief the lower layer is a few islands with thousands of holes, and the whole
|
||||
// of that boundary would otherwise be intersected once per island above.
|
||||
const auto overlaps_nearby = [&]() {
|
||||
for (const ExPolygon &a : expanded) {
|
||||
if (a.empty())
|
||||
continue;
|
||||
const BoundingBox a_bbox = get_extents(a);
|
||||
for (const ExPolygon &b : lower_nearby) {
|
||||
if (b.empty() || !get_extents(b).overlap(a_bbox))
|
||||
continue;
|
||||
const Polygons b_near = ClipperUtils::clip_clipper_polygons_with_subject_bbox(b, a_bbox.inflated(SCALED_EPSILON));
|
||||
if (!intersection_pl(to_polylines(b_near), a).empty() || b.contains(a.contour.points.front()))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (!overlaps_nearby()) {
|
||||
// this is a sharp tail region if it's floating and non-ignorable.
|
||||
// On a belt, "below" has to include the belt itself and the
|
||||
// shear-advanced lower layer, or every belt-contact island reads as
|
||||
// a sharp tail -- which is what the empty-predecessor skip above was
|
||||
// really masking. effective_lower is exactly that notion of below.
|
||||
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
|
||||
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) {
|
||||
is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public:
|
||||
thickness(other.thickness), thickness_layers(other.thickness_layers),
|
||||
bridge_angle(other.bridge_angle), extra_perimeters(other.extra_perimeters)
|
||||
{};
|
||||
Surface(Surface &&rhs) noexcept
|
||||
Surface(Surface &&rhs)
|
||||
: surface_type(rhs.surface_type), expolygon(std::move(rhs.expolygon)),
|
||||
thickness(rhs.thickness), thickness_layers(rhs.thickness_layers),
|
||||
bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters)
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
return *this;
|
||||
}
|
||||
|
||||
Surface& operator=(Surface &&rhs) noexcept
|
||||
Surface& operator=(Surface &&rhs)
|
||||
{
|
||||
surface_type = rhs.surface_type;
|
||||
expolygon = std::move(rhs.expolygon);
|
||||
|
||||
@@ -167,10 +167,10 @@ inline void append(std::vector<T, Alloc> &dest, std::vector<T, Alloc> &&src)
|
||||
{
|
||||
if (dest.empty())
|
||||
dest = std::move(src);
|
||||
else
|
||||
// insert() grows the capacity geometrically; reserving exactly the new size reallocated on every call, which
|
||||
// made appending piece by piece quadratic.
|
||||
dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
|
||||
else {
|
||||
dest.reserve(dest.size() + src.size());
|
||||
std::move(std::begin(src), std::end(src), std::back_inserter(dest));
|
||||
}
|
||||
src.clear();
|
||||
src.shrink_to_fit();
|
||||
}
|
||||
|
||||
@@ -700,7 +700,7 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size)
|
||||
}
|
||||
|
||||
//BBS add render for simple case
|
||||
void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light)
|
||||
void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light, bool ban_color_volume)
|
||||
{
|
||||
if (this->is_left_handed())
|
||||
glFrontFace(GL_CW);
|
||||
@@ -734,7 +734,7 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
|
||||
}
|
||||
} while (0);
|
||||
|
||||
if (color_volume && !picking) {
|
||||
if (color_volume && !picking && !ban_color_volume) {
|
||||
const bool brighten_selected = selected && !disabled && !force_native_color && !force_neutral_color;
|
||||
|
||||
// when force_transparent, we need to keep the alpha
|
||||
|
||||
@@ -370,7 +370,7 @@ public:
|
||||
virtual void render_with_outline(const GUI::Size& cnv_size);
|
||||
|
||||
//BBS: add simple render function for thumbnail
|
||||
void simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light =false);
|
||||
void simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_objects, std::vector<ColorRGBA>& extruder_colors, bool ban_light =false, bool ban_color_volume = false);
|
||||
|
||||
void set_bounding_boxes_as_dirty() {
|
||||
m_transformed_bounding_box.reset();
|
||||
|
||||
@@ -161,7 +161,7 @@ void DailyTipsDataRenderer::render_img(const ImVec2& start_pos, const ImVec2& si
|
||||
|
||||
bool has_cjk(const std::string &text)
|
||||
{
|
||||
for (size_t i = 0; i < text.size() - 2; ++i) {
|
||||
for (size_t i = 0; i + 2 < text.size(); ++i) {
|
||||
unsigned char c1 = text[i];
|
||||
unsigned char c2 = text[i + 1];
|
||||
unsigned char c3 = text[i + 2];
|
||||
|
||||
@@ -2804,7 +2804,9 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
|
||||
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), show_axes);
|
||||
}
|
||||
m_frame_profiler.mark("bed");
|
||||
if (show_bed) //BBS: add outline logic
|
||||
// Exception: the painting gizmos hide the bed mesh on purpose and draw the plate instead,
|
||||
// so there the plate list is the only bed reference in the scene.
|
||||
if (show_bed || _is_below_plate_view_active()) //BBS: add outline logic
|
||||
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
|
||||
if (m_design_canvas && show_bed)
|
||||
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid.
|
||||
@@ -2816,6 +2818,7 @@ void GLCanvas3D::_render_scene(const Camera& camera, const Size& cnv_size)
|
||||
_render_shadows(camera.get_view_matrix(), camera.get_projection_matrix());
|
||||
m_frame_profiler.mark("shadows");
|
||||
_render_objects(GLVolumeCollection::ERenderType::Opaque, !m_gizmos.is_running());
|
||||
_render_below_plate_volumes();
|
||||
m_frame_profiler.mark("objects");
|
||||
_render_sla_slices();
|
||||
_render_selection();
|
||||
@@ -8840,6 +8843,84 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_is_below_plate_view_active() const
|
||||
{
|
||||
return dynamic_cast<const GLGizmoPainterBase*>(m_gizmos.get_current()) != nullptr && m_gizmos.is_hiding_instances();
|
||||
}
|
||||
|
||||
// The painting gizmo clips the selected object at the plate, so the part under the plate is not drawn.
|
||||
// This pass draws that part of the selected instance again at 50% opacity, depth writes off.
|
||||
void GLCanvas3D::_render_below_plate_volumes()
|
||||
{
|
||||
if (!_is_below_plate_view_active() || m_volumes.empty())
|
||||
return;
|
||||
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("gouraud");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||||
const Transform3d& view_matrix = camera.get_view_matrix();
|
||||
const Transform3d& proj_matrix = camera.get_projection_matrix();
|
||||
const int object_idx = m_selection.get_object_idx();
|
||||
const int instance_idx = m_selection.get_instance_idx();
|
||||
|
||||
// Depth test stays on so whatever is already in the depth buffer hides the ghost; depth writes
|
||||
// are off and back faces are culled, so each pixel blends only the near surface once.
|
||||
const GLboolean cull_face = ::glIsEnabled(GL_CULL_FACE);
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
glsafe(::glEnable(GL_CULL_FACE));
|
||||
glsafe(::glCullFace(GL_BACK));
|
||||
glsafe(::glDepthMask(GL_FALSE));
|
||||
glsafe(::glEnable(GL_BLEND));
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
|
||||
shader->start_using();
|
||||
// Keep z <= SINKING_Z_THRESHOLD, the complement of the clip the painting gizmo applies.
|
||||
shader->set_uniform("z_range", std::array<float, 2>{ -FLT_MAX, float(SINKING_Z_THRESHOLD) });
|
||||
shader->set_uniform("clipping_plane", m_volumes.get_clipping_plane());
|
||||
shader->set_uniform("use_color_clip_plane", false);
|
||||
shader->set_uniform("is_outline", false);
|
||||
shader->set_uniform("slope.actived", false);
|
||||
shader->set_uniform("print_volume.type", -1);
|
||||
shader->set_uniform("extruder_printable_heights", std::array<float, 3>{ 0.0f, 0.0f, 0.0f });
|
||||
shader->set_uniform("shadow_intensity", 0.0f);
|
||||
|
||||
// simple_render() needs these to reach the plain surface; with ban_color_volume it ignores both.
|
||||
ModelObjectPtrs& model_objects = wxGetApp().model().objects;
|
||||
std::vector<ColorRGBA> extruder_colors = wxGetApp().plater()->get_extruders_colors();
|
||||
|
||||
for (GLVolume* volume : m_volumes.volumes) {
|
||||
// is_active cannot gate this pass: the color painter marks the whole instance inactive and
|
||||
// draws it in its own gizmo pass. The instance and print-part checks below are what pick
|
||||
// this ghost out of the scene.
|
||||
if (volume->object_idx() != object_idx || volume->instance_idx() != instance_idx ||
|
||||
volume->is_modifier || volume->is_wipe_tower || volume->volume_idx() < 0)
|
||||
continue;
|
||||
|
||||
volume->set_render_color();
|
||||
ColorRGBA color = volume->render_color;
|
||||
color.a(0.5f);
|
||||
volume->model.set_color(color);
|
||||
|
||||
const Transform3d model_matrix = volume->world_matrix();
|
||||
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
|
||||
shader->set_uniform("projection_matrix", proj_matrix);
|
||||
shader->set_uniform("volume_world_matrix", model_matrix);
|
||||
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
shader->set_uniform("view_normal_matrix", view_normal_matrix);
|
||||
// Painted facets would send render() through the MMU segmentation sub-models, which carry
|
||||
// their own opaque filament colours; the ghost draws the plain surface so the alpha holds.
|
||||
volume->simple_render(shader, model_objects, extruder_colors, false, true);
|
||||
}
|
||||
shader->stop_using();
|
||||
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
glsafe(::glDepthMask(GL_TRUE));
|
||||
if (cull_face == GL_FALSE)
|
||||
glsafe(::glDisable(GL_CULL_FACE));
|
||||
}
|
||||
|
||||
bool GLCanvas3D::_hides_inactive_plates() const
|
||||
{
|
||||
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
|
||||
@@ -11968,9 +12049,10 @@ bool GLCanvas3D::is_bed_visible() const
|
||||
if (!m_main_toolbar.is_enabled())
|
||||
return true;
|
||||
|
||||
const auto type = m_gizmos.get_current_type();
|
||||
return type != GLGizmosManager::FdmSupports && type != GLGizmosManager::Seam &&
|
||||
type != GLGizmosManager::MmSegmentation && type != GLGizmosManager::FuzzySkin;
|
||||
// The painting gizmos clip the selected instance at the plate (see InstancesHider), so the bed
|
||||
// mesh would sit in front of the part below it. They draw the plate and its grid instead (see
|
||||
// _render_scene) and the below-plate part as a translucent ghost.
|
||||
return !_is_below_plate_view_active();
|
||||
}
|
||||
|
||||
Vec3d GLCanvas3D::get_camera_pan_anchor(Camera& camera, ECameraNavigationType navigation_type,
|
||||
|
||||
@@ -1552,6 +1552,8 @@ private:
|
||||
void _render_imex_ghost_tooltip();
|
||||
void _render_section_view_caps();
|
||||
void _render_wireframe_overlay();
|
||||
bool _is_below_plate_view_active() const;
|
||||
void _render_below_plate_volumes();
|
||||
bool _is_xray_view_active() const;
|
||||
void _render_xray_volumes();
|
||||
//BBS: GUI refactor: add canvas size as parameters
|
||||
|
||||
@@ -2158,6 +2158,7 @@ wxMenu* MenuFactory::multi_selection_menu()
|
||||
menu->AppendSeparator();
|
||||
|
||||
append_menu_items_convert_unit(menu);
|
||||
append_menu_item_reload_from_disk(menu);
|
||||
append_menu_item_replace_all_with_stl(menu);
|
||||
//BBS
|
||||
append_menu_item_change_filament(menu);
|
||||
@@ -2173,6 +2174,7 @@ wxMenu* MenuFactory::multi_selection_menu()
|
||||
//append_menu_item_simplify(menu);
|
||||
append_menu_item_delete(menu);
|
||||
append_menu_items_convert_unit(menu);
|
||||
append_menu_item_reload_from_disk(menu);
|
||||
append_menu_item_replace_all_with_stl(menu);
|
||||
append_menu_item_change_filament(menu);
|
||||
wxMenu* split_menu = new wxMenu();
|
||||
|
||||
@@ -1089,8 +1089,8 @@ EmbossStyles GLGizmoEmboss::create_default_styles()
|
||||
styles.push_back(WxFontUtils::create_emboss_style(wx_font, _u8L("First font")));
|
||||
} else {
|
||||
// On current OS is not installed any correct TTF font
|
||||
// use font packed with Slic3r
|
||||
std::string font_path = Slic3r::resources_dir() + "/fonts/NotoSans-Regular.ttf";
|
||||
// use font packed with OrcaSlicer
|
||||
std::string font_path = Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf";
|
||||
styles.push_back(EmbossStyle{_u8L("Default font"), font_path, EmbossStyle::Type::file_path});
|
||||
}
|
||||
return styles;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
|
||||
#include <cstddef>
|
||||
#include <glad/gl.h>
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
#include <limits>
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -174,6 +173,9 @@ void InstancesHider::on_update()
|
||||
m_clippers.emplace_back(new MeshClipper);
|
||||
m_clippers.back()->set_plane(ClippingPlane(-Vec3d::UnitZ(), z_min));
|
||||
m_clippers.back()->set_mesh(mesh->its);
|
||||
// Outline only: a filled cap would read as a surface the object does not have, and it
|
||||
// covers the model below it. 0.4 mm matches the cut gizmo's contour width.
|
||||
m_clippers.back()->set_behaviour(false, 0.4);
|
||||
}
|
||||
m_old_meshes = meshes;
|
||||
}
|
||||
@@ -214,10 +216,7 @@ void InstancesHider::render_cut() const
|
||||
else
|
||||
clipper->set_limiting_plane(ClippingPlane::ClipsNothing());
|
||||
|
||||
glsafe(::glPushAttrib(GL_DEPTH_TEST));
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
clipper->render_cut(mv->is_model_part() ? ColorRGBA(0.8f, 0.3f, 0.0f, 1.0f) : color_from_model_volume(*mv));
|
||||
glsafe(::glPopAttrib());
|
||||
clipper->render_contour(mv->is_model_part() ? ColorRGBA(0.8f, 0.3f, 0.0f, 1.0f) : color_from_model_volume(*mv));
|
||||
|
||||
++clipper_id;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ static const char* HMS_LOCAL_IMG_PATH = "hms/local_image";
|
||||
|
||||
// the local HMS info
|
||||
// Orca: dev-id-type set trimmed to the devices Orca ships local HMS images for
|
||||
static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E"};
|
||||
static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E", "20P", "26A", "31B"};
|
||||
|
||||
// Orca: HMS should be disabled when stealth mode is on or networking is not installed
|
||||
static bool should_disable_hms()
|
||||
|
||||
@@ -716,8 +716,6 @@ void PrintJob::process(Ctl &ctl)
|
||||
} else {
|
||||
// wait for printer mqtt ready the same job id
|
||||
|
||||
wxGetApp().plater()->record_slice_preset("print");
|
||||
|
||||
BOOST_LOG_TRIVIAL(error) << "print_job: send ok.";
|
||||
wxCommandEvent* evt = new wxCommandEvent(m_print_job_completed_id);
|
||||
if (!m_completed_evt_data.empty())
|
||||
|
||||
@@ -17866,6 +17866,9 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
|
||||
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
|
||||
print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
|
||||
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
|
||||
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
|
||||
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
|
||||
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
|
||||
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
|
||||
@@ -17930,6 +17933,9 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
|
||||
filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 });
|
||||
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
|
||||
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
|
||||
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
|
||||
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
|
||||
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
|
||||
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
|
||||
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
|
||||
@@ -18671,7 +18677,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect)
|
||||
//BBS: remove GCodeViewer as seperate APP logic
|
||||
bool Plater::load_files(const wxArrayString& filenames)
|
||||
{
|
||||
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc)", std::regex::icase);
|
||||
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc|glb|gltf|fbx)", std::regex::icase);
|
||||
const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase);
|
||||
|
||||
std::vector<fs::path> normal_paths;
|
||||
@@ -20790,76 +20796,6 @@ void Plater::reslice()
|
||||
p->preview->reload_print();
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index();
|
||||
|
||||
record_slice_preset("slicing");
|
||||
}
|
||||
|
||||
void Plater::record_slice_preset(std::string action)
|
||||
{
|
||||
// record slice preset
|
||||
try
|
||||
{
|
||||
json j;
|
||||
auto printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset_with_vendor_profile().preset;
|
||||
if (printer_preset.is_system) {
|
||||
j["printer_preset_name"] = printer_preset.name;
|
||||
}
|
||||
else {
|
||||
j["printer_preset_name"] = printer_preset.config.opt_string("inherits");
|
||||
}
|
||||
const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map;
|
||||
if (keys_map) {
|
||||
for (auto item : *keys_map) {
|
||||
if (item.second == wxGetApp().preset_bundle->project_config.opt_enum<BedType>("curr_bed_type")) {
|
||||
j["curr_bed_type"] = item.first;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto filament_presets = wxGetApp().preset_bundle->filament_presets;
|
||||
for (int i = 0; i < filament_presets.size(); ++i) {
|
||||
auto filament_preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[i]);
|
||||
if (filament_preset->is_system) {
|
||||
j["filament_preset_" + std::to_string(i)] = filament_preset->name;
|
||||
}
|
||||
else {
|
||||
j["filament_preset_" + std::to_string(i)] = filament_preset->config.opt_string("inherits");
|
||||
}
|
||||
}
|
||||
|
||||
Preset& print_preset = wxGetApp().preset_bundle->prints.get_edited_preset();
|
||||
if (print_preset.is_system) {
|
||||
j["process_preset"] = print_preset.name;
|
||||
}
|
||||
else {
|
||||
j["process_preset"] = print_preset.config.opt_string("inherits");
|
||||
}
|
||||
j["support_type"] = ConfigOptionEnum<SupportType>::get_enum_names().at(print_preset.config.opt_enum<SupportType>("support_type"));
|
||||
j["sparse_infill_pattern"] = ConfigOptionEnum<InfillPattern>::get_enum_names().at(print_preset.config.opt_enum<InfillPattern>("sparse_infill_pattern"));
|
||||
j["sparse_infill_density"] = print_preset.config.opt<ConfigOptionPercent>("sparse_infill_density")->value;
|
||||
|
||||
j["brim_type"] = ConfigOptionEnum<BrimType>::get_enum_names().at(print_preset.config.opt_enum<BrimType>("brim_type"));
|
||||
j["user_mode"] = wxGetApp().get_mode_str();
|
||||
|
||||
if (p->background_process.fff_print()) {
|
||||
const DynamicPrintConfig& full_config = p->background_process.fff_print()->full_print_config();
|
||||
json values = json::array();
|
||||
if (full_config.has("different_settings_to_system")) {
|
||||
std::vector<std::string> different_values = full_config.option<ConfigOptionStrings>("different_settings_to_system")->values;
|
||||
for (auto& item : different_values) {
|
||||
values.push_back(item);
|
||||
}
|
||||
}
|
||||
j["different_settings_to_system"] = values;
|
||||
}
|
||||
|
||||
j["record_event"] = action;
|
||||
NetworkAgent* agent = wxGetApp().getAgent();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//BBS: add project slicing related logic
|
||||
|
||||
@@ -597,7 +597,6 @@ public:
|
||||
bool has_toolpaths_to_export() const;
|
||||
void export_toolpaths_to_obj() const;
|
||||
void reslice();
|
||||
void record_slice_preset(std::string action);
|
||||
void reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages = false);
|
||||
void reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages = false);
|
||||
void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false);
|
||||
|
||||
@@ -5738,8 +5738,9 @@ void SelectMachineDialog::reset_and_sync_ams_list()
|
||||
}
|
||||
|
||||
// Orca: collapse the per-nozzle material items into the single panel when the printer has one
|
||||
// extruder or a filament switch (both feed a single logical mapping surface); otherwise keep the
|
||||
// left/right split. Early-returns unless an item is actually in the wrong panel.
|
||||
// extruder or a filament switch (both feed a single logical mapping surface), or when the plate is
|
||||
// sliced for one nozzle (it has no left/right filament map); otherwise keep the left/right split.
|
||||
// Early-returns unless an item is actually in the wrong panel.
|
||||
void SelectMachineDialog::update_material_item_pos(MachineObject* obj_)
|
||||
{
|
||||
if (!obj_) {
|
||||
@@ -5748,7 +5749,10 @@ void SelectMachineDialog::update_material_item_pos(MachineObject* obj_)
|
||||
|
||||
const bool is_single_head = obj_->GetExtderSystem()->GetTotalExtderCount() < 2;
|
||||
const bool has_switcher = obj_->GetFilaSwitch()->IsInstalled();
|
||||
const bool use_single_panel = is_single_head || has_switcher;
|
||||
// reset_and_sync_ams_list() fills m_filaments_map only for a multi-nozzle plate
|
||||
const auto& full_config = wxGetApp().preset_bundle->full_config();
|
||||
const bool single_nozzle_plate = full_config.option<ConfigOptionFloats>("nozzle_diameter")->values.size() < 2;
|
||||
const bool use_single_panel = is_single_head || has_switcher || single_nozzle_plate;
|
||||
|
||||
bool to_change_pos = false;
|
||||
for (const auto& iter : m_materialList) {
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevManager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <boost/format.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
@@ -16,6 +18,7 @@
|
||||
#include <cmath>
|
||||
#include <slic3r/GUI/DeviceManager.hpp>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
using json = nlohmann::json;
|
||||
@@ -64,12 +67,16 @@ const BambuFilamentIdMap& bambu_filament_id_map()
|
||||
return map;
|
||||
}
|
||||
|
||||
// Rewrites every string under "tray_info_idx", "filament_id" or "filamentId", at any depth, in place.
|
||||
// Keys whose string value is a filament id. "cate" and "fila_id" are the H2C nozzle rack's: the
|
||||
// auto nozzle mapping request and the rack status in the push message.
|
||||
constexpr std::array<std::string_view, 5> filament_id_keys{"tray_info_idx", "filament_id", "filamentId", "cate", "fila_id"};
|
||||
|
||||
// Rewrites every string under a filament id key, at any depth, in place.
|
||||
void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::string>& map)
|
||||
{
|
||||
if (j.is_object()) {
|
||||
for (auto& [key, value] : j.items()) {
|
||||
if (value.is_string() && (key == "tray_info_idx" || key == "filament_id" || key == "filamentId")) {
|
||||
if (value.is_string() && std::find(filament_id_keys.begin(), filament_id_keys.end(), key) != filament_id_keys.end()) {
|
||||
auto it = map.find(value.get_ref<const std::string&>());
|
||||
if (it != map.end())
|
||||
value = it->second;
|
||||
@@ -84,8 +91,8 @@ void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::st
|
||||
// Text that does not parse as JSON, or that mentions none of the id keys, comes back byte-identical.
|
||||
std::string rewrite_filament_ids(std::string text, const std::unordered_map<std::string, std::string>& map)
|
||||
{
|
||||
if (map.empty() || (text.find("tray_info_idx") == std::string::npos && text.find("filament_id") == std::string::npos &&
|
||||
text.find("filamentId") == std::string::npos))
|
||||
if (map.empty() || std::none_of(filament_id_keys.begin(), filament_id_keys.end(),
|
||||
[&text](std::string_view key) { return text.find(key) != std::string::npos; }))
|
||||
return text; // nothing to map, skip the parse (moved, not copied)
|
||||
try {
|
||||
json j = json::parse(text);
|
||||
|
||||
@@ -1415,16 +1415,8 @@ void MoonrakerPrinterAgent::announce_printhost_device()
|
||||
}
|
||||
}
|
||||
|
||||
// Try to fetch actual device name from Moonraker
|
||||
// Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback
|
||||
std::string dev_name;
|
||||
MoonrakerDeviceInfo info;
|
||||
std::string fetch_error;
|
||||
if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) {
|
||||
dev_name = info.dev_name;
|
||||
} else {
|
||||
dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
|
||||
}
|
||||
// Never asks the printer, since discovery runs on the GUI thread and an unreachable printer would freeze it
|
||||
const std::string dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
|
||||
|
||||
const std::string model_id = device_info.model_id;
|
||||
|
||||
|
||||
@@ -1917,13 +1917,20 @@ RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& rea
|
||||
|
||||
bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason)
|
||||
{
|
||||
bool needs_refresh = false;
|
||||
{
|
||||
const auto needs_refresh = [&] {
|
||||
std::lock_guard<std::mutex> lock(session_mutex);
|
||||
needs_refresh = should_refresh_locked(skew);
|
||||
}
|
||||
return should_refresh_locked(skew);
|
||||
};
|
||||
if (!needs_refresh())
|
||||
return true;
|
||||
|
||||
if (!needs_refresh)
|
||||
// A refresh may already be running on another thread, e.g. the one start() launches. Wait for
|
||||
// it and check again instead of spending the refresh token it is rotating. Bounded: the
|
||||
// refresh POST times out after 30 s.
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
|
||||
while (refresh_running.load() && std::chrono::steady_clock::now() < deadline)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
if (!needs_refresh())
|
||||
return true;
|
||||
|
||||
// First attempt. refresh_from_storage blocks on the cross-process lock and reads the
|
||||
@@ -1935,6 +1942,8 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
|
||||
// One retry, only for a transient network failure of the refresh POST (lost response,
|
||||
// timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(750));
|
||||
if (!needs_refresh())
|
||||
return true;
|
||||
return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <cstddef>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <ios>
|
||||
#include <slic3r/plugin/PluginDescriptor.hpp>
|
||||
@@ -49,16 +50,22 @@ bool CloudPluginService::fetch_manifests_into_descriptors(std::vector<PluginDesc
|
||||
unauthorized.clear();
|
||||
|
||||
if (m_orca_agent) {
|
||||
// Refresh the token once, then run both requests in parallel.
|
||||
m_orca_agent->ensure_token_fresh("fetch_plugin_manifests");
|
||||
std::vector<PluginDescriptor> mine_descriptors;
|
||||
auto mine = std::async(std::launch::async, [agent = m_orca_agent, &mine_descriptors] {
|
||||
return agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
|
||||
});
|
||||
|
||||
int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized);
|
||||
const int mine_ret = mine.get();
|
||||
|
||||
if (ret != 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<PluginDescriptor> mine_descriptors;
|
||||
|
||||
ret = m_orca_agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
|
||||
ret = mine_ret;
|
||||
|
||||
if (ret != 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins.";
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
`two_color_jpeg.glb` is a generated, closed tetrahedron with four faces, UVs, and
|
||||
an embedded 8×8 red/blue JPEG base-color texture. Its coordinates span 20 mm.
|
||||
|
||||
The core tests load all three fixtures through `Model::read_from_file`, convert
|
||||
textures or MTL colors to painting with boundary smoothing, and match red and blue
|
||||
to filaments. They run in CI without a display. A separate layout check compares
|
||||
the smoothing search-tree type seen by a core consumer with the same type compiled
|
||||
inside `libslic3r_cgal`. The mismatch is checked directly because undefined behavior
|
||||
can be masked by which inline template implementations the linker selects.
|
||||
|
||||
`two_color_sphere/test_two_color_sphere.obj` and its matching MTL are the original
|
||||
red/blue material-color sphere supplied by RF47 in
|
||||
[the PR discussion](https://github.com/OrcaSlicer/OrcaSlicer/pull/16607#issuecomment-6244475095).
|
||||
They contain solid `Kd` colors and no texture images.
|
||||
|
||||
`test_two_color_sphere_textured.obj` is a derived version of that sphere. Its MTL
|
||||
uses white `Kd` values and references `textures/two_color.jpg` by a relative path;
|
||||
the two face groups sample different halves of that image through their UVs.
|
||||
The JPEG is extracted unchanged from `two_color_jpeg.glb`. This case must retain
|
||||
both colors from the external texture, rather than fall back to MTL colors.
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
# test model for OrcaSlicer tex2color
|
||||
newmtl Red
|
||||
Ka 0.0000 0.0000 0.0000
|
||||
Kd 0.7500 0.2500 0.2500
|
||||
Ks 0.0000 0.0000 0.0000
|
||||
d 1.0000
|
||||
newmtl Blue
|
||||
Ka 0.0000 0.0000 0.0000
|
||||
Kd 0.0700 0.2100 0.7000
|
||||
Ks 0.0000 0.0000 0.0000
|
||||
d 1.0000
|
||||
@@ -0,0 +1,457 @@
|
||||
# test model for OrcaSlicer tex2color
|
||||
mtllib test_two_color_sphere.mtl
|
||||
v 0.000000 0.000000 20.000000
|
||||
v 7.653669 0.000000 18.477591
|
||||
v 7.071068 2.928932 18.477591
|
||||
v 5.411961 5.411961 18.477591
|
||||
v 2.928932 7.071068 18.477591
|
||||
v 0.000000 7.653669 18.477591
|
||||
v -2.928932 7.071068 18.477591
|
||||
v -5.411961 5.411961 18.477591
|
||||
v -7.071068 2.928932 18.477591
|
||||
v -7.653669 0.000000 18.477591
|
||||
v -7.071068 -2.928932 18.477591
|
||||
v -5.411961 -5.411961 18.477591
|
||||
v -2.928932 -7.071068 18.477591
|
||||
v -0.000000 -7.653669 18.477591
|
||||
v 2.928932 -7.071068 18.477591
|
||||
v 5.411961 -5.411961 18.477591
|
||||
v 7.071068 -2.928932 18.477591
|
||||
v 14.142136 0.000000 14.142136
|
||||
v 13.065630 5.411961 14.142136
|
||||
v 10.000000 10.000000 14.142136
|
||||
v 5.411961 13.065630 14.142136
|
||||
v 0.000000 14.142136 14.142136
|
||||
v -5.411961 13.065630 14.142136
|
||||
v -10.000000 10.000000 14.142136
|
||||
v -13.065630 5.411961 14.142136
|
||||
v -14.142136 0.000000 14.142136
|
||||
v -13.065630 -5.411961 14.142136
|
||||
v -10.000000 -10.000000 14.142136
|
||||
v -5.411961 -13.065630 14.142136
|
||||
v -0.000000 -14.142136 14.142136
|
||||
v 5.411961 -13.065630 14.142136
|
||||
v 10.000000 -10.000000 14.142136
|
||||
v 13.065630 -5.411961 14.142136
|
||||
v 18.477591 0.000000 7.653669
|
||||
v 17.071068 7.071068 7.653669
|
||||
v 13.065630 13.065630 7.653669
|
||||
v 7.071068 17.071068 7.653669
|
||||
v 0.000000 18.477591 7.653669
|
||||
v -7.071068 17.071068 7.653669
|
||||
v -13.065630 13.065630 7.653669
|
||||
v -17.071068 7.071068 7.653669
|
||||
v -18.477591 0.000000 7.653669
|
||||
v -17.071068 -7.071068 7.653669
|
||||
v -13.065630 -13.065630 7.653669
|
||||
v -7.071068 -17.071068 7.653669
|
||||
v -0.000000 -18.477591 7.653669
|
||||
v 7.071068 -17.071068 7.653669
|
||||
v 13.065630 -13.065630 7.653669
|
||||
v 17.071068 -7.071068 7.653669
|
||||
v 20.000000 0.000000 0.000000
|
||||
v 18.477591 7.653669 0.000000
|
||||
v 14.142136 14.142136 0.000000
|
||||
v 7.653669 18.477591 0.000000
|
||||
v 0.000000 20.000000 0.000000
|
||||
v -7.653669 18.477591 0.000000
|
||||
v -14.142136 14.142136 0.000000
|
||||
v -18.477591 7.653669 0.000000
|
||||
v -20.000000 0.000000 0.000000
|
||||
v -18.477591 -7.653669 0.000000
|
||||
v -14.142136 -14.142136 0.000000
|
||||
v -7.653669 -18.477591 0.000000
|
||||
v -0.000000 -20.000000 0.000000
|
||||
v 7.653669 -18.477591 0.000000
|
||||
v 14.142136 -14.142136 0.000000
|
||||
v 18.477591 -7.653669 0.000000
|
||||
v 18.477591 0.000000 -7.653669
|
||||
v 17.071068 7.071068 -7.653669
|
||||
v 13.065630 13.065630 -7.653669
|
||||
v 7.071068 17.071068 -7.653669
|
||||
v 0.000000 18.477591 -7.653669
|
||||
v -7.071068 17.071068 -7.653669
|
||||
v -13.065630 13.065630 -7.653669
|
||||
v -17.071068 7.071068 -7.653669
|
||||
v -18.477591 0.000000 -7.653669
|
||||
v -17.071068 -7.071068 -7.653669
|
||||
v -13.065630 -13.065630 -7.653669
|
||||
v -7.071068 -17.071068 -7.653669
|
||||
v -0.000000 -18.477591 -7.653669
|
||||
v 7.071068 -17.071068 -7.653669
|
||||
v 13.065630 -13.065630 -7.653669
|
||||
v 17.071068 -7.071068 -7.653669
|
||||
v 14.142136 0.000000 -14.142136
|
||||
v 13.065630 5.411961 -14.142136
|
||||
v 10.000000 10.000000 -14.142136
|
||||
v 5.411961 13.065630 -14.142136
|
||||
v 0.000000 14.142136 -14.142136
|
||||
v -5.411961 13.065630 -14.142136
|
||||
v -10.000000 10.000000 -14.142136
|
||||
v -13.065630 5.411961 -14.142136
|
||||
v -14.142136 0.000000 -14.142136
|
||||
v -13.065630 -5.411961 -14.142136
|
||||
v -10.000000 -10.000000 -14.142136
|
||||
v -5.411961 -13.065630 -14.142136
|
||||
v -0.000000 -14.142136 -14.142136
|
||||
v 5.411961 -13.065630 -14.142136
|
||||
v 10.000000 -10.000000 -14.142136
|
||||
v 13.065630 -5.411961 -14.142136
|
||||
v 7.653669 0.000000 -18.477591
|
||||
v 7.071068 2.928932 -18.477591
|
||||
v 5.411961 5.411961 -18.477591
|
||||
v 2.928932 7.071068 -18.477591
|
||||
v 0.000000 7.653669 -18.477591
|
||||
v -2.928932 7.071068 -18.477591
|
||||
v -5.411961 5.411961 -18.477591
|
||||
v -7.071068 2.928932 -18.477591
|
||||
v -7.653669 0.000000 -18.477591
|
||||
v -7.071068 -2.928932 -18.477591
|
||||
v -5.411961 -5.411961 -18.477591
|
||||
v -2.928932 -7.071068 -18.477591
|
||||
v -0.000000 -7.653669 -18.477591
|
||||
v 2.928932 -7.071068 -18.477591
|
||||
v 5.411961 -5.411961 -18.477591
|
||||
v 7.071068 -2.928932 -18.477591
|
||||
v 0.000000 0.000000 -20.000000
|
||||
vt 0 0
|
||||
vn 0.000000 -0.000000 1.000000
|
||||
vn 0.439605 0.035510 0.897489
|
||||
vn 0.392553 0.201037 0.897489
|
||||
vn 0.285739 0.335958 0.897489
|
||||
vn 0.135423 0.419732 0.897489
|
||||
vn -0.035510 0.439605 0.897489
|
||||
vn -0.201037 0.392553 0.897489
|
||||
vn -0.335958 0.285739 0.897489
|
||||
vn -0.419732 0.135423 0.897489
|
||||
vn -0.439605 -0.035510 0.897489
|
||||
vn -0.392553 -0.201037 0.897489
|
||||
vn -0.285739 -0.335958 0.897489
|
||||
vn -0.135423 -0.419732 0.897489
|
||||
vn 0.035510 -0.439605 0.897489
|
||||
vn 0.201037 -0.392553 0.897489
|
||||
vn 0.335958 -0.285739 0.897489
|
||||
vn 0.419732 -0.135423 0.897489
|
||||
vn 0.725001 0.027230 0.688209
|
||||
vn 0.659393 0.302603 0.688209
|
||||
vn 0.493399 0.531907 0.688209
|
||||
vn 0.252289 0.680234 0.688209
|
||||
vn -0.027230 0.725001 0.688209
|
||||
vn -0.302603 0.659393 0.688209
|
||||
vn -0.531907 0.493399 0.688209
|
||||
vn -0.680234 0.252289 0.688209
|
||||
vn -0.725001 -0.027230 0.688209
|
||||
vn -0.659393 -0.302603 0.688209
|
||||
vn -0.493399 -0.531907 0.688209
|
||||
vn -0.252289 -0.680234 0.688209
|
||||
vn 0.027230 -0.725001 0.688209
|
||||
vn 0.302603 -0.659393 0.688209
|
||||
vn 0.531907 -0.493399 0.688209
|
||||
vn 0.680234 -0.252289 0.688209
|
||||
vn 0.927851 0.014745 0.372658
|
||||
vn 0.851580 0.368696 0.372658
|
||||
vn 0.645664 0.666516 0.372658
|
||||
vn 0.341451 0.862865 0.372658
|
||||
vn -0.014745 0.927851 0.372658
|
||||
vn -0.368696 0.851580 0.372658
|
||||
vn -0.666516 0.645664 0.372658
|
||||
vn -0.862865 0.341451 0.372658
|
||||
vn -0.927851 -0.014745 0.372658
|
||||
vn -0.851580 -0.368696 0.372658
|
||||
vn -0.645664 -0.666516 0.372658
|
||||
vn -0.341451 -0.862865 0.372658
|
||||
vn 0.014745 -0.927851 0.372658
|
||||
vn 0.368696 -0.851580 0.372658
|
||||
vn 0.666516 -0.645664 0.372658
|
||||
vn 0.862865 -0.341451 0.372658
|
||||
vn 1.000000 -0.000000 -0.000000
|
||||
vn 0.923880 0.382683 0.000000
|
||||
vn 0.707107 0.707107 0.000000
|
||||
vn 0.382683 0.923880 0.000000
|
||||
vn 0.000000 1.000000 0.000000
|
||||
vn -0.382683 0.923880 0.000000
|
||||
vn -0.707107 0.707107 0.000000
|
||||
vn -0.923880 0.382683 0.000000
|
||||
vn -1.000000 0.000000 0.000000
|
||||
vn -0.923880 -0.382683 -0.000000
|
||||
vn -0.707107 -0.707107 0.000000
|
||||
vn -0.382683 -0.923880 -0.000000
|
||||
vn -0.000000 -1.000000 -0.000000
|
||||
vn 0.382683 -0.923880 0.000000
|
||||
vn 0.707107 -0.707107 0.000000
|
||||
vn 0.923880 -0.382683 -0.000000
|
||||
vn 0.927851 -0.014745 -0.372658
|
||||
vn 0.862865 0.341451 -0.372658
|
||||
vn 0.666516 0.645664 -0.372658
|
||||
vn 0.368696 0.851580 -0.372658
|
||||
vn 0.014745 0.927851 -0.372658
|
||||
vn -0.341451 0.862865 -0.372658
|
||||
vn -0.645664 0.666516 -0.372658
|
||||
vn -0.851580 0.368696 -0.372658
|
||||
vn -0.927851 0.014745 -0.372658
|
||||
vn -0.862865 -0.341451 -0.372658
|
||||
vn -0.666516 -0.645664 -0.372658
|
||||
vn -0.368696 -0.851580 -0.372658
|
||||
vn -0.014745 -0.927851 -0.372658
|
||||
vn 0.341451 -0.862865 -0.372658
|
||||
vn 0.645664 -0.666516 -0.372658
|
||||
vn 0.851580 -0.368696 -0.372658
|
||||
vn 0.725001 -0.027230 -0.688209
|
||||
vn 0.680234 0.252289 -0.688209
|
||||
vn 0.531907 0.493399 -0.688209
|
||||
vn 0.302603 0.659393 -0.688209
|
||||
vn 0.027230 0.725001 -0.688209
|
||||
vn -0.252289 0.680234 -0.688209
|
||||
vn -0.493399 0.531907 -0.688209
|
||||
vn -0.659393 0.302603 -0.688209
|
||||
vn -0.725001 0.027230 -0.688209
|
||||
vn -0.680234 -0.252289 -0.688209
|
||||
vn -0.531907 -0.493399 -0.688209
|
||||
vn -0.302603 -0.659393 -0.688209
|
||||
vn -0.027230 -0.725001 -0.688209
|
||||
vn 0.252289 -0.680234 -0.688209
|
||||
vn 0.493399 -0.531907 -0.688209
|
||||
vn 0.659393 -0.302603 -0.688209
|
||||
vn 0.439605 -0.035510 -0.897489
|
||||
vn 0.419732 0.135423 -0.897489
|
||||
vn 0.335958 0.285739 -0.897489
|
||||
vn 0.201037 0.392553 -0.897489
|
||||
vn 0.035510 0.439605 -0.897489
|
||||
vn -0.135423 0.419732 -0.897489
|
||||
vn -0.285739 0.335958 -0.897489
|
||||
vn -0.392553 0.201037 -0.897489
|
||||
vn -0.439605 0.035510 -0.897489
|
||||
vn -0.419732 -0.135423 -0.897489
|
||||
vn -0.335958 -0.285739 -0.897489
|
||||
vn -0.201037 -0.392553 -0.897489
|
||||
vn -0.035510 -0.439605 -0.897489
|
||||
vn 0.135423 -0.419732 -0.897489
|
||||
vn 0.285739 -0.335958 -0.897489
|
||||
vn 0.392553 -0.201037 -0.897489
|
||||
vn 0.000000 -0.000000 -1.000000
|
||||
usemtl Red
|
||||
f 1/1/1 2/1/2 3/1/3
|
||||
f 1/1/1 3/1/3 4/1/4
|
||||
f 1/1/1 4/1/4 5/1/5
|
||||
f 1/1/1 5/1/5 6/1/6
|
||||
f 1/1/1 6/1/6 7/1/7
|
||||
f 1/1/1 7/1/7 8/1/8
|
||||
f 1/1/1 8/1/8 9/1/9
|
||||
f 1/1/1 9/1/9 10/1/10
|
||||
f 1/1/1 10/1/10 11/1/11
|
||||
f 1/1/1 11/1/11 12/1/12
|
||||
f 1/1/1 12/1/12 13/1/13
|
||||
f 1/1/1 13/1/13 14/1/14
|
||||
f 1/1/1 14/1/14 15/1/15
|
||||
f 1/1/1 15/1/15 16/1/16
|
||||
f 1/1/1 16/1/16 17/1/17
|
||||
f 1/1/1 17/1/17 2/1/2
|
||||
f 2/1/2 18/1/18 19/1/19
|
||||
f 2/1/2 19/1/19 3/1/3
|
||||
f 3/1/3 19/1/19 20/1/20
|
||||
f 3/1/3 20/1/20 4/1/4
|
||||
f 4/1/4 20/1/20 21/1/21
|
||||
f 4/1/4 21/1/21 5/1/5
|
||||
f 5/1/5 21/1/21 22/1/22
|
||||
f 5/1/5 22/1/22 6/1/6
|
||||
f 6/1/6 22/1/22 23/1/23
|
||||
f 6/1/6 23/1/23 7/1/7
|
||||
f 7/1/7 23/1/23 24/1/24
|
||||
f 7/1/7 24/1/24 8/1/8
|
||||
f 8/1/8 24/1/24 25/1/25
|
||||
f 8/1/8 25/1/25 9/1/9
|
||||
f 9/1/9 25/1/25 26/1/26
|
||||
f 9/1/9 26/1/26 10/1/10
|
||||
f 10/1/10 26/1/26 27/1/27
|
||||
f 10/1/10 27/1/27 11/1/11
|
||||
f 11/1/11 27/1/27 28/1/28
|
||||
f 11/1/11 28/1/28 12/1/12
|
||||
f 12/1/12 28/1/28 29/1/29
|
||||
f 12/1/12 29/1/29 13/1/13
|
||||
f 13/1/13 29/1/29 30/1/30
|
||||
f 13/1/13 30/1/30 14/1/14
|
||||
f 14/1/14 30/1/30 31/1/31
|
||||
f 14/1/14 31/1/31 15/1/15
|
||||
f 15/1/15 31/1/31 32/1/32
|
||||
f 15/1/15 32/1/32 16/1/16
|
||||
f 16/1/16 32/1/32 33/1/33
|
||||
f 16/1/16 33/1/33 17/1/17
|
||||
f 17/1/17 33/1/33 18/1/18
|
||||
f 17/1/17 18/1/18 2/1/2
|
||||
f 18/1/18 34/1/34 35/1/35
|
||||
f 18/1/18 35/1/35 19/1/19
|
||||
f 19/1/19 35/1/35 36/1/36
|
||||
f 19/1/19 36/1/36 20/1/20
|
||||
f 20/1/20 36/1/36 37/1/37
|
||||
f 20/1/20 37/1/37 21/1/21
|
||||
f 21/1/21 37/1/37 38/1/38
|
||||
f 21/1/21 38/1/38 22/1/22
|
||||
f 22/1/22 38/1/38 39/1/39
|
||||
f 22/1/22 39/1/39 23/1/23
|
||||
f 23/1/23 39/1/39 40/1/40
|
||||
f 23/1/23 40/1/40 24/1/24
|
||||
f 24/1/24 40/1/40 41/1/41
|
||||
f 24/1/24 41/1/41 25/1/25
|
||||
f 25/1/25 41/1/41 42/1/42
|
||||
f 25/1/25 42/1/42 26/1/26
|
||||
f 26/1/26 42/1/42 43/1/43
|
||||
f 26/1/26 43/1/43 27/1/27
|
||||
f 27/1/27 43/1/43 44/1/44
|
||||
f 27/1/27 44/1/44 28/1/28
|
||||
f 28/1/28 44/1/44 45/1/45
|
||||
f 28/1/28 45/1/45 29/1/29
|
||||
f 29/1/29 45/1/45 46/1/46
|
||||
f 29/1/29 46/1/46 30/1/30
|
||||
f 30/1/30 46/1/46 47/1/47
|
||||
f 30/1/30 47/1/47 31/1/31
|
||||
f 31/1/31 47/1/47 48/1/48
|
||||
f 31/1/31 48/1/48 32/1/32
|
||||
f 32/1/32 48/1/48 49/1/49
|
||||
f 32/1/32 49/1/49 33/1/33
|
||||
f 33/1/33 49/1/49 34/1/34
|
||||
f 33/1/33 34/1/34 18/1/18
|
||||
f 34/1/34 50/1/50 51/1/51
|
||||
f 34/1/34 51/1/51 35/1/35
|
||||
f 35/1/35 51/1/51 52/1/52
|
||||
f 35/1/35 52/1/52 36/1/36
|
||||
f 36/1/36 52/1/52 53/1/53
|
||||
f 36/1/36 53/1/53 37/1/37
|
||||
f 37/1/37 53/1/53 54/1/54
|
||||
f 37/1/37 54/1/54 38/1/38
|
||||
f 38/1/38 54/1/54 55/1/55
|
||||
f 38/1/38 55/1/55 39/1/39
|
||||
f 39/1/39 55/1/55 56/1/56
|
||||
f 39/1/39 56/1/56 40/1/40
|
||||
f 40/1/40 56/1/56 57/1/57
|
||||
f 40/1/40 57/1/57 41/1/41
|
||||
f 41/1/41 57/1/57 58/1/58
|
||||
f 41/1/41 58/1/58 42/1/42
|
||||
f 42/1/42 58/1/58 59/1/59
|
||||
f 42/1/42 59/1/59 43/1/43
|
||||
f 43/1/43 59/1/59 60/1/60
|
||||
f 43/1/43 60/1/60 44/1/44
|
||||
f 44/1/44 60/1/60 61/1/61
|
||||
f 44/1/44 61/1/61 45/1/45
|
||||
f 45/1/45 61/1/61 62/1/62
|
||||
f 45/1/45 62/1/62 46/1/46
|
||||
f 46/1/46 62/1/62 63/1/63
|
||||
f 46/1/46 63/1/63 47/1/47
|
||||
f 47/1/47 63/1/63 64/1/64
|
||||
f 47/1/47 64/1/64 48/1/48
|
||||
f 48/1/48 64/1/64 65/1/65
|
||||
f 48/1/48 65/1/65 49/1/49
|
||||
f 49/1/49 65/1/65 50/1/50
|
||||
f 49/1/49 50/1/50 34/1/34
|
||||
usemtl Blue
|
||||
f 50/1/50 66/1/66 67/1/67
|
||||
f 50/1/50 67/1/67 51/1/51
|
||||
f 51/1/51 67/1/67 68/1/68
|
||||
f 51/1/51 68/1/68 52/1/52
|
||||
f 52/1/52 68/1/68 69/1/69
|
||||
f 52/1/52 69/1/69 53/1/53
|
||||
f 53/1/53 69/1/69 70/1/70
|
||||
f 53/1/53 70/1/70 54/1/54
|
||||
f 54/1/54 70/1/70 71/1/71
|
||||
f 54/1/54 71/1/71 55/1/55
|
||||
f 55/1/55 71/1/71 72/1/72
|
||||
f 55/1/55 72/1/72 56/1/56
|
||||
f 56/1/56 72/1/72 73/1/73
|
||||
f 56/1/56 73/1/73 57/1/57
|
||||
f 57/1/57 73/1/73 74/1/74
|
||||
f 57/1/57 74/1/74 58/1/58
|
||||
f 58/1/58 74/1/74 75/1/75
|
||||
f 58/1/58 75/1/75 59/1/59
|
||||
f 59/1/59 75/1/75 76/1/76
|
||||
f 59/1/59 76/1/76 60/1/60
|
||||
f 60/1/60 76/1/76 77/1/77
|
||||
f 60/1/60 77/1/77 61/1/61
|
||||
f 61/1/61 77/1/77 78/1/78
|
||||
f 61/1/61 78/1/78 62/1/62
|
||||
f 62/1/62 78/1/78 79/1/79
|
||||
f 62/1/62 79/1/79 63/1/63
|
||||
f 63/1/63 79/1/79 80/1/80
|
||||
f 63/1/63 80/1/80 64/1/64
|
||||
f 64/1/64 80/1/80 81/1/81
|
||||
f 64/1/64 81/1/81 65/1/65
|
||||
f 65/1/65 81/1/81 66/1/66
|
||||
f 65/1/65 66/1/66 50/1/50
|
||||
f 66/1/66 82/1/82 83/1/83
|
||||
f 66/1/66 83/1/83 67/1/67
|
||||
f 67/1/67 83/1/83 84/1/84
|
||||
f 67/1/67 84/1/84 68/1/68
|
||||
f 68/1/68 84/1/84 85/1/85
|
||||
f 68/1/68 85/1/85 69/1/69
|
||||
f 69/1/69 85/1/85 86/1/86
|
||||
f 69/1/69 86/1/86 70/1/70
|
||||
f 70/1/70 86/1/86 87/1/87
|
||||
f 70/1/70 87/1/87 71/1/71
|
||||
f 71/1/71 87/1/87 88/1/88
|
||||
f 71/1/71 88/1/88 72/1/72
|
||||
f 72/1/72 88/1/88 89/1/89
|
||||
f 72/1/72 89/1/89 73/1/73
|
||||
f 73/1/73 89/1/89 90/1/90
|
||||
f 73/1/73 90/1/90 74/1/74
|
||||
f 74/1/74 90/1/90 91/1/91
|
||||
f 74/1/74 91/1/91 75/1/75
|
||||
f 75/1/75 91/1/91 92/1/92
|
||||
f 75/1/75 92/1/92 76/1/76
|
||||
f 76/1/76 92/1/92 93/1/93
|
||||
f 76/1/76 93/1/93 77/1/77
|
||||
f 77/1/77 93/1/93 94/1/94
|
||||
f 77/1/77 94/1/94 78/1/78
|
||||
f 78/1/78 94/1/94 95/1/95
|
||||
f 78/1/78 95/1/95 79/1/79
|
||||
f 79/1/79 95/1/95 96/1/96
|
||||
f 79/1/79 96/1/96 80/1/80
|
||||
f 80/1/80 96/1/96 97/1/97
|
||||
f 80/1/80 97/1/97 81/1/81
|
||||
f 81/1/81 97/1/97 82/1/82
|
||||
f 81/1/81 82/1/82 66/1/66
|
||||
f 82/1/82 98/1/98 99/1/99
|
||||
f 82/1/82 99/1/99 83/1/83
|
||||
f 83/1/83 99/1/99 100/1/100
|
||||
f 83/1/83 100/1/100 84/1/84
|
||||
f 84/1/84 100/1/100 101/1/101
|
||||
f 84/1/84 101/1/101 85/1/85
|
||||
f 85/1/85 101/1/101 102/1/102
|
||||
f 85/1/85 102/1/102 86/1/86
|
||||
f 86/1/86 102/1/102 103/1/103
|
||||
f 86/1/86 103/1/103 87/1/87
|
||||
f 87/1/87 103/1/103 104/1/104
|
||||
f 87/1/87 104/1/104 88/1/88
|
||||
f 88/1/88 104/1/104 105/1/105
|
||||
f 88/1/88 105/1/105 89/1/89
|
||||
f 89/1/89 105/1/105 106/1/106
|
||||
f 89/1/89 106/1/106 90/1/90
|
||||
f 90/1/90 106/1/106 107/1/107
|
||||
f 90/1/90 107/1/107 91/1/91
|
||||
f 91/1/91 107/1/107 108/1/108
|
||||
f 91/1/91 108/1/108 92/1/92
|
||||
f 92/1/92 108/1/108 109/1/109
|
||||
f 92/1/92 109/1/109 93/1/93
|
||||
f 93/1/93 109/1/109 110/1/110
|
||||
f 93/1/93 110/1/110 94/1/94
|
||||
f 94/1/94 110/1/110 111/1/111
|
||||
f 94/1/94 111/1/111 95/1/95
|
||||
f 95/1/95 111/1/111 112/1/112
|
||||
f 95/1/95 112/1/112 96/1/96
|
||||
f 96/1/96 112/1/112 113/1/113
|
||||
f 96/1/96 113/1/113 97/1/97
|
||||
f 97/1/97 113/1/113 98/1/98
|
||||
f 97/1/97 98/1/98 82/1/82
|
||||
f 114/1/114 99/1/99 98/1/98
|
||||
f 114/1/114 100/1/100 99/1/99
|
||||
f 114/1/114 101/1/101 100/1/100
|
||||
f 114/1/114 102/1/102 101/1/101
|
||||
f 114/1/114 103/1/103 102/1/102
|
||||
f 114/1/114 104/1/104 103/1/103
|
||||
f 114/1/114 105/1/105 104/1/104
|
||||
f 114/1/114 106/1/106 105/1/105
|
||||
f 114/1/114 107/1/107 106/1/106
|
||||
f 114/1/114 108/1/108 107/1/107
|
||||
f 114/1/114 109/1/109 108/1/108
|
||||
f 114/1/114 110/1/110 109/1/109
|
||||
f 114/1/114 111/1/111 110/1/110
|
||||
f 114/1/114 112/1/112 111/1/111
|
||||
f 114/1/114 113/1/113 112/1/112
|
||||
f 114/1/114 98/1/98 113/1/113
|
||||
@@ -0,0 +1,8 @@
|
||||
# Derived texture-file regression: color comes from map_Kd, not Kd.
|
||||
newmtl Red
|
||||
Kd 1 1 1
|
||||
map_Kd textures/two_color.jpg
|
||||
|
||||
newmtl Blue
|
||||
Kd 1 1 1
|
||||
map_Kd textures/two_color.jpg
|
||||
@@ -0,0 +1,458 @@
|
||||
# test model for OrcaSlicer tex2color
|
||||
mtllib test_two_color_sphere_textured.mtl
|
||||
v 0.000000 0.000000 20.000000
|
||||
v 7.653669 0.000000 18.477591
|
||||
v 7.071068 2.928932 18.477591
|
||||
v 5.411961 5.411961 18.477591
|
||||
v 2.928932 7.071068 18.477591
|
||||
v 0.000000 7.653669 18.477591
|
||||
v -2.928932 7.071068 18.477591
|
||||
v -5.411961 5.411961 18.477591
|
||||
v -7.071068 2.928932 18.477591
|
||||
v -7.653669 0.000000 18.477591
|
||||
v -7.071068 -2.928932 18.477591
|
||||
v -5.411961 -5.411961 18.477591
|
||||
v -2.928932 -7.071068 18.477591
|
||||
v -0.000000 -7.653669 18.477591
|
||||
v 2.928932 -7.071068 18.477591
|
||||
v 5.411961 -5.411961 18.477591
|
||||
v 7.071068 -2.928932 18.477591
|
||||
v 14.142136 0.000000 14.142136
|
||||
v 13.065630 5.411961 14.142136
|
||||
v 10.000000 10.000000 14.142136
|
||||
v 5.411961 13.065630 14.142136
|
||||
v 0.000000 14.142136 14.142136
|
||||
v -5.411961 13.065630 14.142136
|
||||
v -10.000000 10.000000 14.142136
|
||||
v -13.065630 5.411961 14.142136
|
||||
v -14.142136 0.000000 14.142136
|
||||
v -13.065630 -5.411961 14.142136
|
||||
v -10.000000 -10.000000 14.142136
|
||||
v -5.411961 -13.065630 14.142136
|
||||
v -0.000000 -14.142136 14.142136
|
||||
v 5.411961 -13.065630 14.142136
|
||||
v 10.000000 -10.000000 14.142136
|
||||
v 13.065630 -5.411961 14.142136
|
||||
v 18.477591 0.000000 7.653669
|
||||
v 17.071068 7.071068 7.653669
|
||||
v 13.065630 13.065630 7.653669
|
||||
v 7.071068 17.071068 7.653669
|
||||
v 0.000000 18.477591 7.653669
|
||||
v -7.071068 17.071068 7.653669
|
||||
v -13.065630 13.065630 7.653669
|
||||
v -17.071068 7.071068 7.653669
|
||||
v -18.477591 0.000000 7.653669
|
||||
v -17.071068 -7.071068 7.653669
|
||||
v -13.065630 -13.065630 7.653669
|
||||
v -7.071068 -17.071068 7.653669
|
||||
v -0.000000 -18.477591 7.653669
|
||||
v 7.071068 -17.071068 7.653669
|
||||
v 13.065630 -13.065630 7.653669
|
||||
v 17.071068 -7.071068 7.653669
|
||||
v 20.000000 0.000000 0.000000
|
||||
v 18.477591 7.653669 0.000000
|
||||
v 14.142136 14.142136 0.000000
|
||||
v 7.653669 18.477591 0.000000
|
||||
v 0.000000 20.000000 0.000000
|
||||
v -7.653669 18.477591 0.000000
|
||||
v -14.142136 14.142136 0.000000
|
||||
v -18.477591 7.653669 0.000000
|
||||
v -20.000000 0.000000 0.000000
|
||||
v -18.477591 -7.653669 0.000000
|
||||
v -14.142136 -14.142136 0.000000
|
||||
v -7.653669 -18.477591 0.000000
|
||||
v -0.000000 -20.000000 0.000000
|
||||
v 7.653669 -18.477591 0.000000
|
||||
v 14.142136 -14.142136 0.000000
|
||||
v 18.477591 -7.653669 0.000000
|
||||
v 18.477591 0.000000 -7.653669
|
||||
v 17.071068 7.071068 -7.653669
|
||||
v 13.065630 13.065630 -7.653669
|
||||
v 7.071068 17.071068 -7.653669
|
||||
v 0.000000 18.477591 -7.653669
|
||||
v -7.071068 17.071068 -7.653669
|
||||
v -13.065630 13.065630 -7.653669
|
||||
v -17.071068 7.071068 -7.653669
|
||||
v -18.477591 0.000000 -7.653669
|
||||
v -17.071068 -7.071068 -7.653669
|
||||
v -13.065630 -13.065630 -7.653669
|
||||
v -7.071068 -17.071068 -7.653669
|
||||
v -0.000000 -18.477591 -7.653669
|
||||
v 7.071068 -17.071068 -7.653669
|
||||
v 13.065630 -13.065630 -7.653669
|
||||
v 17.071068 -7.071068 -7.653669
|
||||
v 14.142136 0.000000 -14.142136
|
||||
v 13.065630 5.411961 -14.142136
|
||||
v 10.000000 10.000000 -14.142136
|
||||
v 5.411961 13.065630 -14.142136
|
||||
v 0.000000 14.142136 -14.142136
|
||||
v -5.411961 13.065630 -14.142136
|
||||
v -10.000000 10.000000 -14.142136
|
||||
v -13.065630 5.411961 -14.142136
|
||||
v -14.142136 0.000000 -14.142136
|
||||
v -13.065630 -5.411961 -14.142136
|
||||
v -10.000000 -10.000000 -14.142136
|
||||
v -5.411961 -13.065630 -14.142136
|
||||
v -0.000000 -14.142136 -14.142136
|
||||
v 5.411961 -13.065630 -14.142136
|
||||
v 10.000000 -10.000000 -14.142136
|
||||
v 13.065630 -5.411961 -14.142136
|
||||
v 7.653669 0.000000 -18.477591
|
||||
v 7.071068 2.928932 -18.477591
|
||||
v 5.411961 5.411961 -18.477591
|
||||
v 2.928932 7.071068 -18.477591
|
||||
v 0.000000 7.653669 -18.477591
|
||||
v -2.928932 7.071068 -18.477591
|
||||
v -5.411961 5.411961 -18.477591
|
||||
v -7.071068 2.928932 -18.477591
|
||||
v -7.653669 0.000000 -18.477591
|
||||
v -7.071068 -2.928932 -18.477591
|
||||
v -5.411961 -5.411961 -18.477591
|
||||
v -2.928932 -7.071068 -18.477591
|
||||
v -0.000000 -7.653669 -18.477591
|
||||
v 2.928932 -7.071068 -18.477591
|
||||
v 5.411961 -5.411961 -18.477591
|
||||
v 7.071068 -2.928932 -18.477591
|
||||
v 0.000000 0.000000 -20.000000
|
||||
vt 0.25 0.5
|
||||
vt 0.75 0.5
|
||||
vn 0.000000 -0.000000 1.000000
|
||||
vn 0.439605 0.035510 0.897489
|
||||
vn 0.392553 0.201037 0.897489
|
||||
vn 0.285739 0.335958 0.897489
|
||||
vn 0.135423 0.419732 0.897489
|
||||
vn -0.035510 0.439605 0.897489
|
||||
vn -0.201037 0.392553 0.897489
|
||||
vn -0.335958 0.285739 0.897489
|
||||
vn -0.419732 0.135423 0.897489
|
||||
vn -0.439605 -0.035510 0.897489
|
||||
vn -0.392553 -0.201037 0.897489
|
||||
vn -0.285739 -0.335958 0.897489
|
||||
vn -0.135423 -0.419732 0.897489
|
||||
vn 0.035510 -0.439605 0.897489
|
||||
vn 0.201037 -0.392553 0.897489
|
||||
vn 0.335958 -0.285739 0.897489
|
||||
vn 0.419732 -0.135423 0.897489
|
||||
vn 0.725001 0.027230 0.688209
|
||||
vn 0.659393 0.302603 0.688209
|
||||
vn 0.493399 0.531907 0.688209
|
||||
vn 0.252289 0.680234 0.688209
|
||||
vn -0.027230 0.725001 0.688209
|
||||
vn -0.302603 0.659393 0.688209
|
||||
vn -0.531907 0.493399 0.688209
|
||||
vn -0.680234 0.252289 0.688209
|
||||
vn -0.725001 -0.027230 0.688209
|
||||
vn -0.659393 -0.302603 0.688209
|
||||
vn -0.493399 -0.531907 0.688209
|
||||
vn -0.252289 -0.680234 0.688209
|
||||
vn 0.027230 -0.725001 0.688209
|
||||
vn 0.302603 -0.659393 0.688209
|
||||
vn 0.531907 -0.493399 0.688209
|
||||
vn 0.680234 -0.252289 0.688209
|
||||
vn 0.927851 0.014745 0.372658
|
||||
vn 0.851580 0.368696 0.372658
|
||||
vn 0.645664 0.666516 0.372658
|
||||
vn 0.341451 0.862865 0.372658
|
||||
vn -0.014745 0.927851 0.372658
|
||||
vn -0.368696 0.851580 0.372658
|
||||
vn -0.666516 0.645664 0.372658
|
||||
vn -0.862865 0.341451 0.372658
|
||||
vn -0.927851 -0.014745 0.372658
|
||||
vn -0.851580 -0.368696 0.372658
|
||||
vn -0.645664 -0.666516 0.372658
|
||||
vn -0.341451 -0.862865 0.372658
|
||||
vn 0.014745 -0.927851 0.372658
|
||||
vn 0.368696 -0.851580 0.372658
|
||||
vn 0.666516 -0.645664 0.372658
|
||||
vn 0.862865 -0.341451 0.372658
|
||||
vn 1.000000 -0.000000 -0.000000
|
||||
vn 0.923880 0.382683 0.000000
|
||||
vn 0.707107 0.707107 0.000000
|
||||
vn 0.382683 0.923880 0.000000
|
||||
vn 0.000000 1.000000 0.000000
|
||||
vn -0.382683 0.923880 0.000000
|
||||
vn -0.707107 0.707107 0.000000
|
||||
vn -0.923880 0.382683 0.000000
|
||||
vn -1.000000 0.000000 0.000000
|
||||
vn -0.923880 -0.382683 -0.000000
|
||||
vn -0.707107 -0.707107 0.000000
|
||||
vn -0.382683 -0.923880 -0.000000
|
||||
vn -0.000000 -1.000000 -0.000000
|
||||
vn 0.382683 -0.923880 0.000000
|
||||
vn 0.707107 -0.707107 0.000000
|
||||
vn 0.923880 -0.382683 -0.000000
|
||||
vn 0.927851 -0.014745 -0.372658
|
||||
vn 0.862865 0.341451 -0.372658
|
||||
vn 0.666516 0.645664 -0.372658
|
||||
vn 0.368696 0.851580 -0.372658
|
||||
vn 0.014745 0.927851 -0.372658
|
||||
vn -0.341451 0.862865 -0.372658
|
||||
vn -0.645664 0.666516 -0.372658
|
||||
vn -0.851580 0.368696 -0.372658
|
||||
vn -0.927851 0.014745 -0.372658
|
||||
vn -0.862865 -0.341451 -0.372658
|
||||
vn -0.666516 -0.645664 -0.372658
|
||||
vn -0.368696 -0.851580 -0.372658
|
||||
vn -0.014745 -0.927851 -0.372658
|
||||
vn 0.341451 -0.862865 -0.372658
|
||||
vn 0.645664 -0.666516 -0.372658
|
||||
vn 0.851580 -0.368696 -0.372658
|
||||
vn 0.725001 -0.027230 -0.688209
|
||||
vn 0.680234 0.252289 -0.688209
|
||||
vn 0.531907 0.493399 -0.688209
|
||||
vn 0.302603 0.659393 -0.688209
|
||||
vn 0.027230 0.725001 -0.688209
|
||||
vn -0.252289 0.680234 -0.688209
|
||||
vn -0.493399 0.531907 -0.688209
|
||||
vn -0.659393 0.302603 -0.688209
|
||||
vn -0.725001 0.027230 -0.688209
|
||||
vn -0.680234 -0.252289 -0.688209
|
||||
vn -0.531907 -0.493399 -0.688209
|
||||
vn -0.302603 -0.659393 -0.688209
|
||||
vn -0.027230 -0.725001 -0.688209
|
||||
vn 0.252289 -0.680234 -0.688209
|
||||
vn 0.493399 -0.531907 -0.688209
|
||||
vn 0.659393 -0.302603 -0.688209
|
||||
vn 0.439605 -0.035510 -0.897489
|
||||
vn 0.419732 0.135423 -0.897489
|
||||
vn 0.335958 0.285739 -0.897489
|
||||
vn 0.201037 0.392553 -0.897489
|
||||
vn 0.035510 0.439605 -0.897489
|
||||
vn -0.135423 0.419732 -0.897489
|
||||
vn -0.285739 0.335958 -0.897489
|
||||
vn -0.392553 0.201037 -0.897489
|
||||
vn -0.439605 0.035510 -0.897489
|
||||
vn -0.419732 -0.135423 -0.897489
|
||||
vn -0.335958 -0.285739 -0.897489
|
||||
vn -0.201037 -0.392553 -0.897489
|
||||
vn -0.035510 -0.439605 -0.897489
|
||||
vn 0.135423 -0.419732 -0.897489
|
||||
vn 0.285739 -0.335958 -0.897489
|
||||
vn 0.392553 -0.201037 -0.897489
|
||||
vn 0.000000 -0.000000 -1.000000
|
||||
usemtl Red
|
||||
f 1/1/1 2/1/2 3/1/3
|
||||
f 1/1/1 3/1/3 4/1/4
|
||||
f 1/1/1 4/1/4 5/1/5
|
||||
f 1/1/1 5/1/5 6/1/6
|
||||
f 1/1/1 6/1/6 7/1/7
|
||||
f 1/1/1 7/1/7 8/1/8
|
||||
f 1/1/1 8/1/8 9/1/9
|
||||
f 1/1/1 9/1/9 10/1/10
|
||||
f 1/1/1 10/1/10 11/1/11
|
||||
f 1/1/1 11/1/11 12/1/12
|
||||
f 1/1/1 12/1/12 13/1/13
|
||||
f 1/1/1 13/1/13 14/1/14
|
||||
f 1/1/1 14/1/14 15/1/15
|
||||
f 1/1/1 15/1/15 16/1/16
|
||||
f 1/1/1 16/1/16 17/1/17
|
||||
f 1/1/1 17/1/17 2/1/2
|
||||
f 2/1/2 18/1/18 19/1/19
|
||||
f 2/1/2 19/1/19 3/1/3
|
||||
f 3/1/3 19/1/19 20/1/20
|
||||
f 3/1/3 20/1/20 4/1/4
|
||||
f 4/1/4 20/1/20 21/1/21
|
||||
f 4/1/4 21/1/21 5/1/5
|
||||
f 5/1/5 21/1/21 22/1/22
|
||||
f 5/1/5 22/1/22 6/1/6
|
||||
f 6/1/6 22/1/22 23/1/23
|
||||
f 6/1/6 23/1/23 7/1/7
|
||||
f 7/1/7 23/1/23 24/1/24
|
||||
f 7/1/7 24/1/24 8/1/8
|
||||
f 8/1/8 24/1/24 25/1/25
|
||||
f 8/1/8 25/1/25 9/1/9
|
||||
f 9/1/9 25/1/25 26/1/26
|
||||
f 9/1/9 26/1/26 10/1/10
|
||||
f 10/1/10 26/1/26 27/1/27
|
||||
f 10/1/10 27/1/27 11/1/11
|
||||
f 11/1/11 27/1/27 28/1/28
|
||||
f 11/1/11 28/1/28 12/1/12
|
||||
f 12/1/12 28/1/28 29/1/29
|
||||
f 12/1/12 29/1/29 13/1/13
|
||||
f 13/1/13 29/1/29 30/1/30
|
||||
f 13/1/13 30/1/30 14/1/14
|
||||
f 14/1/14 30/1/30 31/1/31
|
||||
f 14/1/14 31/1/31 15/1/15
|
||||
f 15/1/15 31/1/31 32/1/32
|
||||
f 15/1/15 32/1/32 16/1/16
|
||||
f 16/1/16 32/1/32 33/1/33
|
||||
f 16/1/16 33/1/33 17/1/17
|
||||
f 17/1/17 33/1/33 18/1/18
|
||||
f 17/1/17 18/1/18 2/1/2
|
||||
f 18/1/18 34/1/34 35/1/35
|
||||
f 18/1/18 35/1/35 19/1/19
|
||||
f 19/1/19 35/1/35 36/1/36
|
||||
f 19/1/19 36/1/36 20/1/20
|
||||
f 20/1/20 36/1/36 37/1/37
|
||||
f 20/1/20 37/1/37 21/1/21
|
||||
f 21/1/21 37/1/37 38/1/38
|
||||
f 21/1/21 38/1/38 22/1/22
|
||||
f 22/1/22 38/1/38 39/1/39
|
||||
f 22/1/22 39/1/39 23/1/23
|
||||
f 23/1/23 39/1/39 40/1/40
|
||||
f 23/1/23 40/1/40 24/1/24
|
||||
f 24/1/24 40/1/40 41/1/41
|
||||
f 24/1/24 41/1/41 25/1/25
|
||||
f 25/1/25 41/1/41 42/1/42
|
||||
f 25/1/25 42/1/42 26/1/26
|
||||
f 26/1/26 42/1/42 43/1/43
|
||||
f 26/1/26 43/1/43 27/1/27
|
||||
f 27/1/27 43/1/43 44/1/44
|
||||
f 27/1/27 44/1/44 28/1/28
|
||||
f 28/1/28 44/1/44 45/1/45
|
||||
f 28/1/28 45/1/45 29/1/29
|
||||
f 29/1/29 45/1/45 46/1/46
|
||||
f 29/1/29 46/1/46 30/1/30
|
||||
f 30/1/30 46/1/46 47/1/47
|
||||
f 30/1/30 47/1/47 31/1/31
|
||||
f 31/1/31 47/1/47 48/1/48
|
||||
f 31/1/31 48/1/48 32/1/32
|
||||
f 32/1/32 48/1/48 49/1/49
|
||||
f 32/1/32 49/1/49 33/1/33
|
||||
f 33/1/33 49/1/49 34/1/34
|
||||
f 33/1/33 34/1/34 18/1/18
|
||||
f 34/1/34 50/1/50 51/1/51
|
||||
f 34/1/34 51/1/51 35/1/35
|
||||
f 35/1/35 51/1/51 52/1/52
|
||||
f 35/1/35 52/1/52 36/1/36
|
||||
f 36/1/36 52/1/52 53/1/53
|
||||
f 36/1/36 53/1/53 37/1/37
|
||||
f 37/1/37 53/1/53 54/1/54
|
||||
f 37/1/37 54/1/54 38/1/38
|
||||
f 38/1/38 54/1/54 55/1/55
|
||||
f 38/1/38 55/1/55 39/1/39
|
||||
f 39/1/39 55/1/55 56/1/56
|
||||
f 39/1/39 56/1/56 40/1/40
|
||||
f 40/1/40 56/1/56 57/1/57
|
||||
f 40/1/40 57/1/57 41/1/41
|
||||
f 41/1/41 57/1/57 58/1/58
|
||||
f 41/1/41 58/1/58 42/1/42
|
||||
f 42/1/42 58/1/58 59/1/59
|
||||
f 42/1/42 59/1/59 43/1/43
|
||||
f 43/1/43 59/1/59 60/1/60
|
||||
f 43/1/43 60/1/60 44/1/44
|
||||
f 44/1/44 60/1/60 61/1/61
|
||||
f 44/1/44 61/1/61 45/1/45
|
||||
f 45/1/45 61/1/61 62/1/62
|
||||
f 45/1/45 62/1/62 46/1/46
|
||||
f 46/1/46 62/1/62 63/1/63
|
||||
f 46/1/46 63/1/63 47/1/47
|
||||
f 47/1/47 63/1/63 64/1/64
|
||||
f 47/1/47 64/1/64 48/1/48
|
||||
f 48/1/48 64/1/64 65/1/65
|
||||
f 48/1/48 65/1/65 49/1/49
|
||||
f 49/1/49 65/1/65 50/1/50
|
||||
f 49/1/49 50/1/50 34/1/34
|
||||
usemtl Blue
|
||||
f 50/2/50 66/2/66 67/2/67
|
||||
f 50/2/50 67/2/67 51/2/51
|
||||
f 51/2/51 67/2/67 68/2/68
|
||||
f 51/2/51 68/2/68 52/2/52
|
||||
f 52/2/52 68/2/68 69/2/69
|
||||
f 52/2/52 69/2/69 53/2/53
|
||||
f 53/2/53 69/2/69 70/2/70
|
||||
f 53/2/53 70/2/70 54/2/54
|
||||
f 54/2/54 70/2/70 71/2/71
|
||||
f 54/2/54 71/2/71 55/2/55
|
||||
f 55/2/55 71/2/71 72/2/72
|
||||
f 55/2/55 72/2/72 56/2/56
|
||||
f 56/2/56 72/2/72 73/2/73
|
||||
f 56/2/56 73/2/73 57/2/57
|
||||
f 57/2/57 73/2/73 74/2/74
|
||||
f 57/2/57 74/2/74 58/2/58
|
||||
f 58/2/58 74/2/74 75/2/75
|
||||
f 58/2/58 75/2/75 59/2/59
|
||||
f 59/2/59 75/2/75 76/2/76
|
||||
f 59/2/59 76/2/76 60/2/60
|
||||
f 60/2/60 76/2/76 77/2/77
|
||||
f 60/2/60 77/2/77 61/2/61
|
||||
f 61/2/61 77/2/77 78/2/78
|
||||
f 61/2/61 78/2/78 62/2/62
|
||||
f 62/2/62 78/2/78 79/2/79
|
||||
f 62/2/62 79/2/79 63/2/63
|
||||
f 63/2/63 79/2/79 80/2/80
|
||||
f 63/2/63 80/2/80 64/2/64
|
||||
f 64/2/64 80/2/80 81/2/81
|
||||
f 64/2/64 81/2/81 65/2/65
|
||||
f 65/2/65 81/2/81 66/2/66
|
||||
f 65/2/65 66/2/66 50/2/50
|
||||
f 66/2/66 82/2/82 83/2/83
|
||||
f 66/2/66 83/2/83 67/2/67
|
||||
f 67/2/67 83/2/83 84/2/84
|
||||
f 67/2/67 84/2/84 68/2/68
|
||||
f 68/2/68 84/2/84 85/2/85
|
||||
f 68/2/68 85/2/85 69/2/69
|
||||
f 69/2/69 85/2/85 86/2/86
|
||||
f 69/2/69 86/2/86 70/2/70
|
||||
f 70/2/70 86/2/86 87/2/87
|
||||
f 70/2/70 87/2/87 71/2/71
|
||||
f 71/2/71 87/2/87 88/2/88
|
||||
f 71/2/71 88/2/88 72/2/72
|
||||
f 72/2/72 88/2/88 89/2/89
|
||||
f 72/2/72 89/2/89 73/2/73
|
||||
f 73/2/73 89/2/89 90/2/90
|
||||
f 73/2/73 90/2/90 74/2/74
|
||||
f 74/2/74 90/2/90 91/2/91
|
||||
f 74/2/74 91/2/91 75/2/75
|
||||
f 75/2/75 91/2/91 92/2/92
|
||||
f 75/2/75 92/2/92 76/2/76
|
||||
f 76/2/76 92/2/92 93/2/93
|
||||
f 76/2/76 93/2/93 77/2/77
|
||||
f 77/2/77 93/2/93 94/2/94
|
||||
f 77/2/77 94/2/94 78/2/78
|
||||
f 78/2/78 94/2/94 95/2/95
|
||||
f 78/2/78 95/2/95 79/2/79
|
||||
f 79/2/79 95/2/95 96/2/96
|
||||
f 79/2/79 96/2/96 80/2/80
|
||||
f 80/2/80 96/2/96 97/2/97
|
||||
f 80/2/80 97/2/97 81/2/81
|
||||
f 81/2/81 97/2/97 82/2/82
|
||||
f 81/2/81 82/2/82 66/2/66
|
||||
f 82/2/82 98/2/98 99/2/99
|
||||
f 82/2/82 99/2/99 83/2/83
|
||||
f 83/2/83 99/2/99 100/2/100
|
||||
f 83/2/83 100/2/100 84/2/84
|
||||
f 84/2/84 100/2/100 101/2/101
|
||||
f 84/2/84 101/2/101 85/2/85
|
||||
f 85/2/85 101/2/101 102/2/102
|
||||
f 85/2/85 102/2/102 86/2/86
|
||||
f 86/2/86 102/2/102 103/2/103
|
||||
f 86/2/86 103/2/103 87/2/87
|
||||
f 87/2/87 103/2/103 104/2/104
|
||||
f 87/2/87 104/2/104 88/2/88
|
||||
f 88/2/88 104/2/104 105/2/105
|
||||
f 88/2/88 105/2/105 89/2/89
|
||||
f 89/2/89 105/2/105 106/2/106
|
||||
f 89/2/89 106/2/106 90/2/90
|
||||
f 90/2/90 106/2/106 107/2/107
|
||||
f 90/2/90 107/2/107 91/2/91
|
||||
f 91/2/91 107/2/107 108/2/108
|
||||
f 91/2/91 108/2/108 92/2/92
|
||||
f 92/2/92 108/2/108 109/2/109
|
||||
f 92/2/92 109/2/109 93/2/93
|
||||
f 93/2/93 109/2/109 110/2/110
|
||||
f 93/2/93 110/2/110 94/2/94
|
||||
f 94/2/94 110/2/110 111/2/111
|
||||
f 94/2/94 111/2/111 95/2/95
|
||||
f 95/2/95 111/2/111 112/2/112
|
||||
f 95/2/95 112/2/112 96/2/96
|
||||
f 96/2/96 112/2/112 113/2/113
|
||||
f 96/2/96 113/2/113 97/2/97
|
||||
f 97/2/97 113/2/113 98/2/98
|
||||
f 97/2/97 98/2/98 82/2/82
|
||||
f 114/2/114 99/2/99 98/2/98
|
||||
f 114/2/114 100/2/100 99/2/99
|
||||
f 114/2/114 101/2/101 100/2/100
|
||||
f 114/2/114 102/2/102 101/2/101
|
||||
f 114/2/114 103/2/103 102/2/102
|
||||
f 114/2/114 104/2/104 103/2/103
|
||||
f 114/2/114 105/2/105 104/2/104
|
||||
f 114/2/114 106/2/106 105/2/105
|
||||
f 114/2/114 107/2/107 106/2/106
|
||||
f 114/2/114 108/2/108 107/2/107
|
||||
f 114/2/114 109/2/109 108/2/108
|
||||
f 114/2/114 110/2/110 109/2/109
|
||||
f 114/2/114 111/2/111 110/2/110
|
||||
f 114/2/114 112/2/112 111/2/111
|
||||
f 114/2/114 113/2/113 112/2/112
|
||||
f 114/2/114 98/2/98 113/2/113
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
@@ -1719,9 +1719,10 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "
|
||||
const SparseInfillShape smooth = shape_for("100%");
|
||||
|
||||
REQUIRE(sharp.path_count > 0);
|
||||
// Smoothing should leave the loop count about where it was; uncapping the smoothing reach, the
|
||||
// regression this guards against, adds about 10%.
|
||||
const size_t allowed_extra = sharp.path_count / 50; // 2%
|
||||
// Smoothing should leave the path count about where it was; uncapping the smoothing reach, the
|
||||
// regression this guards against, adds a quarter to a third. Outline ends next to each other are
|
||||
// joined into one path, so a few loops more or less are a larger share of the count.
|
||||
const size_t allowed_extra = sharp.path_count / 10; // 10%
|
||||
REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra);
|
||||
// The outlines are still rounded.
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
@@ -2353,3 +2354,66 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
|
||||
max_distance = std::max(max_distance, printed_tree.distance_from_lines<false>(point));
|
||||
CHECK(unscale<double>(max_distance) <= 0.012);
|
||||
}
|
||||
|
||||
TEST_CASE("Infill lines whose ends are next to each other join into one path", "[Fill]")
|
||||
{
|
||||
// An out-and-back pair 1 mm apart, as multiline infill leaves, and a line 19 mm away.
|
||||
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
|
||||
Polyline(Point::new_scale(0., 20.), Point::new_scale(10., 20.)) };
|
||||
join_close_ends(lines, scale_(1.5), true);
|
||||
|
||||
REQUIRE(lines.size() == 2);
|
||||
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
|
||||
REQUIRE(joined != lines.end());
|
||||
// Both lines plus the 1 mm between them, joined at one end only, as joining the other end too would close a loop.
|
||||
CHECK_THAT(unscale<double>(joined->length()), Catch::Matchers::WithinAbs(21., 1e-6));
|
||||
CHECK(joined->first_point() != joined->last_point());
|
||||
}
|
||||
|
||||
TEST_CASE("Joined infill lines print each line once", "[Fill]")
|
||||
{
|
||||
// The middle line comes first and both of its ends get joined, so the group has to be walked from an outer line.
|
||||
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
|
||||
Polyline(Point::new_scale(10., -1.), Point::new_scale(0., -1.)) };
|
||||
join_close_ends(lines, scale_(1.5), true);
|
||||
|
||||
REQUIRE(lines.size() == 1);
|
||||
CHECK(lines.front().size() == 6);
|
||||
// Three 10 mm lines and two 1 mm joins.
|
||||
CHECK_THAT(unscale<double>(lines.front().length()), Catch::Matchers::WithinAbs(32., 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Infill line ends are not joined across another line", "[Fill]")
|
||||
{
|
||||
// A pair 1 mm apart whose left ends have a third line running between them, which a join there would cross.
|
||||
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
|
||||
Polyline(Point::new_scale(-5., 0.5), Point::new_scale(5., 0.5)) };
|
||||
join_close_ends(lines, scale_(1.5), true);
|
||||
|
||||
REQUIRE(lines.size() == 2);
|
||||
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
|
||||
REQUIRE(joined != lines.end());
|
||||
// Joined on the right, where nothing is in between, so the left ends stay open.
|
||||
CHECK(joined->first_point().x() == 0);
|
||||
CHECK(joined->last_point().x() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Infill lines that cannot reverse join only end to start", "[Fill]")
|
||||
{
|
||||
const Point a(Point::new_scale(0., 0.)), b(Point::new_scale(10., 0.)), c(Point::new_scale(10., 1.)), d(Point::new_scale(0., 1.));
|
||||
SECTION("Ends that would need a reversal stay apart") {
|
||||
Polylines lines { Polyline(a, b), Polyline(d, c) };
|
||||
join_close_ends(lines, scale_(1.5), false);
|
||||
CHECK(lines.size() == 2);
|
||||
}
|
||||
SECTION("The end of one line joins the start of the next, both kept in their direction") {
|
||||
Polylines lines { Polyline(c, d), Polyline(a, b) };
|
||||
join_close_ends(lines, scale_(1.5), false);
|
||||
REQUIRE(lines.size() == 1);
|
||||
const Points &points = lines.front().points;
|
||||
REQUIRE(points.size() == 4);
|
||||
auto position = [&points](const Point &p) { return std::find(points.begin(), points.end(), p) - points.begin(); };
|
||||
CHECK(position(a) < position(b));
|
||||
CHECK(position(c) < position(d));
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -30,6 +33,7 @@ namespace Slic3r { enum ExtrusionRole : uint8_t; }
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
// Regression coverage for filament/tool-change time being folded into the first
|
||||
// pending motion block (an extrusion move) instead of the tool-change move, and
|
||||
@@ -570,6 +574,33 @@ double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double j
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Klipper extrusion ratio changes use its own corner velocity, not E jerk", "[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
const double jerk_e = GENERATE(0.0, 100.0);
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_jerk_e.values = {jerk_e, jerk_e};
|
||||
GCodeProcessor processor;
|
||||
run_processor(processor, config,
|
||||
"G90\nM83\nSET_VELOCITY_LIMIT ACCEL=1000 MINIMUM_CRUISE_RATIO=0\n"
|
||||
"G1 X20 Y60 F6000\nM400\n"
|
||||
"G1 X60 Y60 E0.8 F6000\nG1 X100 Y60 E1.6 F6000\nM400\n");
|
||||
// A 0.02 -> 0.04 extrusion ratio change is capped at 1 / 0.02 = 50 mm/s.
|
||||
REQUIRE_THAT(corner_speed(processor.get_result()), Catch::Matchers::WithinAbs(50.0, 0.01));
|
||||
}
|
||||
|
||||
TEST_CASE("Klipper legacy smoothing stays absolute across acceleration changes", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
const bool modern = GENERATE(false, true);
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
const std::string gcode = std::string("G90\nSET_VELOCITY_LIMIT ACCEL=1000 ") +
|
||||
(modern ? "MINIMUM_CRUISE_RATIO=0.75\n" : "ACCEL_TO_DECEL=250\n") +
|
||||
"G1 X40 F30000\nM400\nM204 S2000\nG1 X0\nM400\n";
|
||||
GCodeProcessor processor;
|
||||
run_processor(processor, config, gcode.c_str());
|
||||
const double expected = modern ? 0.5 + 0.25 * std::sqrt(2.0) : 0.95;
|
||||
REQUIRE_THAT(processor.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinAbs(expected, 1e-5));
|
||||
}
|
||||
|
||||
TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity",
|
||||
"[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
@@ -669,3 +700,172 @@ TEST_CASE("How fast a corner is taken does not depend on how much is extruded th
|
||||
Catch::Matchers::WithinRel(marlin, 0.02));
|
||||
}
|
||||
}
|
||||
|
||||
// Expected times are Klipper's own planner math (klippy/toolhead.py). For a move from rest to rest,
|
||||
// LookAheadQueue.flush() reduces to cruise_v2 = min(vmax^2, d * accel, d * pseudo_accel) with
|
||||
// pseudo_accel = accel * (1 - minimum_cruise_ratio), and Move.set_junction() to a trapezoid at accel;
|
||||
// a move too short to reach its speed still spends that ratio of its length cruising.
|
||||
namespace {
|
||||
|
||||
constexpr double klipper_accel = 1000.0; // make_junction_config's acceleration
|
||||
constexpr double klipper_cruise_ratio = 0.5;
|
||||
constexpr double klipper_pseudo_accel = klipper_accel * (1.0 - klipper_cruise_ratio);
|
||||
|
||||
// Time of a move that starts and ends at rest.
|
||||
double rest_to_rest_time(double distance, double max_speed, double accel, double pseudo_accel)
|
||||
{
|
||||
const double cruise_v2 = std::min(sqr(max_speed), distance * pseudo_accel);
|
||||
const double cruise = std::sqrt(cruise_v2);
|
||||
return 2.0 * cruise / accel + (distance - cruise_v2 / accel) / cruise;
|
||||
}
|
||||
|
||||
// Every run states the cruise ratio rather than relying on the estimator's built-in default.
|
||||
void run_klipper(GCodeProcessor& proc, const std::string& gcode,
|
||||
const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
|
||||
{
|
||||
const std::string limits = "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) + "\n";
|
||||
run_processor(proc, config, (limits + gcode).c_str());
|
||||
}
|
||||
|
||||
double klipper_time(const std::string& gcode, const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
|
||||
{
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, gcode, config);
|
||||
return proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
|
||||
}
|
||||
|
||||
double time_after_retract(const GCodeProcessorResult& r)
|
||||
{
|
||||
double t = 0.0;
|
||||
bool after = false;
|
||||
for (const auto& mv : r.moves) {
|
||||
if (mv.type == EMoveType::Retract)
|
||||
after = true;
|
||||
else if (after)
|
||||
t += mv.time[NORMAL];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A straight line split into short segments is planned as one move", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// 100 mm at 50 mm/s queues 2 s of motion, so the look-ahead flushes lazily part-way along the
|
||||
// line; at 10 mm/s^2 the ramps span many segments and the cruise ratio caps the speed.
|
||||
const int segments = GENERATE(1, 100, 1000);
|
||||
std::ostringstream gcode;
|
||||
gcode << "SET_VELOCITY_LIMIT ACCEL=10\n" << std::fixed << std::setprecision(3);
|
||||
for (int i = 1; i <= segments; ++i)
|
||||
gcode << "G1 X" << 100.0 * i / segments << " F3000\n";
|
||||
REQUIRE_THAT(klipper_time(gcode.str()),
|
||||
WithinRel(rest_to_rest_time(100.0, 50.0, 10.0, 10.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Where the look-ahead flushes does not change the estimate", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// A retraction stops the toolhead, so the zig-zag after it is planned on its own; the straight run
|
||||
// before it only shifts where the queue flushes lazily (1 s of motion after a stop, then every 0.15 s).
|
||||
auto zigzag_time = [](int run_moves) {
|
||||
std::string gcode = "M83\nG91\n";
|
||||
for (int i = 0; i < run_moves; ++i)
|
||||
gcode += "G1 X1 F6000\n";
|
||||
gcode += "G1 E-1 F2400\n";
|
||||
const char* moves[] = {"G1 X2 F3000\n", "G1 Y2 F9000\n", "G1 X2 F1200\n", "G1 Y-2 F9000\n"};
|
||||
for (int i = 0; i < 24; ++i)
|
||||
gcode += moves[i % 4];
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, gcode);
|
||||
return time_after_retract(proc.get_result());
|
||||
};
|
||||
const double reference = zigzag_time(0);
|
||||
REQUIRE(reference > 0.0);
|
||||
for (int run_moves : {30, 80, 105, 130}) {
|
||||
INFO("run moves = " << run_moves);
|
||||
REQUIRE_THAT(zigzag_time(run_moves), WithinAbs(reference, 1e-9));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A short move into a corner is limited by the centripetal term, not the corner velocity",
|
||||
"[GCodeTiming][Klipper]")
|
||||
{
|
||||
// A 50 mm/s square corner velocity allows 50 mm/s through the right angle; the 1 mm move into it
|
||||
// allows sqrt(0.5 * 1 * 1000 * tan(45 deg)) = 22.4 mm/s (Move.calc_junction's move_centripetal_v2).
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, "G90\nG1 X59 Y60 F6000\nM400\nG1 X60 Y60 F9000\nG1 X60 Y100 F9000\n",
|
||||
make_junction_config(gcfKlipper, 50.0, 0.0));
|
||||
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(std::sqrt(500.0), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A corner keeps the square corner velocity when the acceleration changes between its moves",
|
||||
"[GCodeTiming][Klipper]")
|
||||
{
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, "G90\nG1 X20 Y60 F6000\nM400\nG1 X60 Y60 F9000\nM204 S4000\nG1 X60 Y100 F9000\n");
|
||||
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(5.0, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A retraction stops the toolhead and runs at the extruder's own limits", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// An E acceleration of 0 is unset, and the retraction then uses the retract acceleration.
|
||||
const auto [e_accel, retract_accel] = GENERATE(table<double, double>({{20000.0, 20000.0}, {0.0, 3000.0}}));
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_acceleration_e.values = {e_accel, e_accel};
|
||||
config.machine_max_acceleration_retracting.values = {3000.0, 3000.0};
|
||||
GCodeProcessor proc;
|
||||
run_klipper(proc, "G90\nM83\nG1 X60 Y60 F9000\nG1 E-1 F2400\nG1 X100 Y60 F9000\n", config);
|
||||
REQUIRE_THAT(corner_speed(proc.get_result()), WithinAbs(0.0, 1e-6));
|
||||
const double expected = rest_to_rest_time(std::sqrt(2.0) * 60.0, 150.0, klipper_accel, klipper_pseudo_accel) +
|
||||
rest_to_rest_time(1.0, 40.0, retract_accel, std::min(retract_accel, klipper_pseudo_accel)) +
|
||||
rest_to_rest_time(40.0, 150.0, klipper_accel, klipper_pseudo_accel);
|
||||
REQUIRE_THAT(proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinRel(expected, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Extruder limits apply to retractions, not to extrusion along a path", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_speed_e.values = {10.0, 10.0};
|
||||
config.machine_max_acceleration_e.values = {10.0, 10.0};
|
||||
REQUIRE_THAT(klipper_time("M83\nG1 X40 E20 F30000\n", config),
|
||||
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
|
||||
REQUIRE_THAT(klipper_time("M83\nG1 E-1 F2400\n", config),
|
||||
WithinRel(rest_to_rest_time(1.0, 10.0, 10.0, 10.0), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A Z move is limited by the Z axis speed and acceleration", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
|
||||
config.machine_max_speed_z.values = {20.0, 20.0};
|
||||
config.machine_max_acceleration_z.values = {100.0, 100.0};
|
||||
REQUIRE_THAT(klipper_time("G1 Z10 F1200\n", config),
|
||||
WithinRel(rest_to_rest_time(10.0, 20.0, 100.0, 100.0), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("M204 sets the acceleration from S, or the smaller of P and T", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
const char* m204 = GENERATE("M204 S500", "M204 P500 T2000", "M204 P2000 T500");
|
||||
REQUIRE_THAT(klipper_time(std::string(m204) + "\nG1 X100 F30000\n"),
|
||||
WithinRel(rest_to_rest_time(100.0, 500.0, 500.0, 500.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("SET_VELOCITY_LIMIT applies every limit on the line and ignores the comment", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT VELOCITY=80 ACCEL=500 MINIMUM_CRUISE_RATIO=0.2 ; ACCEL=9999\n"
|
||||
"G1 X20 F30000\n"),
|
||||
WithinRel(rest_to_rest_time(20.0, 80.0, 500.0, 500.0 * (1.0 - 0.2)), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("ACCEL_TO_DECEL is ignored on a line that also sets the minimum cruise ratio", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
// Klipper 0.13 prefers the cruise ratio and later releases no longer read ACCEL_TO_DECEL at all.
|
||||
// ACCEL_TO_DECEL comes last, so applying the parameters in line order would let it win.
|
||||
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) +
|
||||
" ACCEL_TO_DECEL=100\nG1 X40 F30000\n"),
|
||||
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("M400 without a delay waits for the queue to drain", "[GCodeTiming][Klipper]")
|
||||
{
|
||||
REQUIRE_THAT(klipper_time("G90\nG1 X40 F30000\nM400\nG1 X80\n"),
|
||||
WithinRel(2.0 * rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
|
||||
}
|
||||
@@ -514,7 +514,7 @@ TEST_CASE("Acceleration and velocity limit commands print their values in genera
|
||||
const Case c = GENERATE(values<Case>({
|
||||
{gcfKlipper, Command::KlipperLimits, 2000000, 25. / 3., false,
|
||||
{"SET_VELOCITY_LIMIT ACCEL=2000000 ", "ACCEL_TO_DECEL=1e+06 ", "SQUARE_CORNER_VELOCITY=8.33333\n"}, {}},
|
||||
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
|
||||
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5 ", "MINIMUM_CRUISE_RATIO=0.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
|
||||
{gcfKlipper, Command::KlipperLimits, 0, 0.25, true, {"SQUARE_CORNER_VELOCITY=0.25 ", "; adjust VELOCITY_LIMIT"}, {"ACCEL"}},
|
||||
{gcfKlipper, Command::Print, 3001, 0., true, {"ACCEL=3001 ", "ACCEL_TO_DECEL=1500.5 ", "; adjust ACCEL_TO_DECEL", "; adjust acceleration"}, {}},
|
||||
{gcfMarlinFirmware, Command::Print, 2500, 0., false, {"M204 P2500\n"}, {}},
|
||||
@@ -1272,6 +1272,41 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G
|
||||
REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("Klipper acceleration commands send accel_to_decel both as ACCEL_TO_DECEL and as the cruise ratio", "[GCodeWriter]")
|
||||
{
|
||||
// Klipper 0.13 defines MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL; older releases read only the
|
||||
// first parameter and current ones only the second, so both have to describe the same limit.
|
||||
GCodeWriter writer;
|
||||
writer.config.gcode_flavor.value = gcfKlipper;
|
||||
writer.config.accel_to_decel_enable.value = true;
|
||||
// The writer appends "; adjust ..." comments by default; keep them out so the line ends at the ratio.
|
||||
struct CommentGuard
|
||||
{
|
||||
bool saved = GCodeWriter::full_gcode_comment;
|
||||
~CommentGuard() { GCodeWriter::full_gcode_comment = saved; }
|
||||
} comment_guard;
|
||||
GCodeWriter::full_gcode_comment = false;
|
||||
|
||||
SECTION("a 25% factor") {
|
||||
writer.config.accel_to_decel_factor.value = 25;
|
||||
REQUIRE_THAT(writer.set_print_acceleration(2000),
|
||||
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=500 MINIMUM_CRUISE_RATIO=0.75"));
|
||||
REQUIRE_THAT(writer.set_accel_and_jerk(3000, 0.0),
|
||||
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=3000 ACCEL_TO_DECEL=750 MINIMUM_CRUISE_RATIO=0.75"));
|
||||
}
|
||||
SECTION("a 100% factor turns the cruise ratio off") {
|
||||
writer.config.accel_to_decel_factor.value = 100;
|
||||
REQUIRE_THAT(writer.set_print_acceleration(2000),
|
||||
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=2000 MINIMUM_CRUISE_RATIO=0\n"));
|
||||
}
|
||||
SECTION("neither is sent while the option is off") {
|
||||
writer.config.accel_to_decel_enable.value = false;
|
||||
const std::string line = writer.set_print_acceleration(2000);
|
||||
REQUIRE_THAT(line, Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000"));
|
||||
REQUIRE_THAT(line, !Catch::Matchers::ContainsSubstring("ACCEL_TO_DECEL") && !Catch::Matchers::ContainsSubstring("MINIMUM_CRUISE_RATIO"));
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test for the belt-printer "illegal gantry move at print start" bug.
|
||||
//
|
||||
// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed
|
||||
|
||||
@@ -202,6 +202,23 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("The wipe tower sets Klipper's acceleration and accel_to_decel on one line", "[WipeTower]")
|
||||
{
|
||||
// On separate lines, current Klipper finds nothing it reads on the ACCEL_TO_DECEL one and 0.13
|
||||
// derives the cruise ratio from the acceleration before the change.
|
||||
DynamicPrintConfig config = wipe_tower_toolchange_config("klipper");
|
||||
// Type 2 towers leave the acceleration to the main G-code writer.
|
||||
config.set_deserialize_strict({ { "wipe_tower_type", "type1" },
|
||||
{ "accel_to_decel_enable", "1" }, { "accel_to_decel_factor", "50%" },
|
||||
{ "default_acceleration", "1000" }, { "travel_acceleration", "2000" },
|
||||
{ "machine_max_acceleration_extruding", "5000" } });
|
||||
const std::string tower = wipe_tower_regions(slice_with_prime_tower(config));
|
||||
REQUIRE_THAT(tower, Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL="));
|
||||
CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(" MINIMUM_CRUISE_RATIO=0.5\n"));
|
||||
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL_TO_DECEL") &&
|
||||
!Catch::Matchers::ContainsSubstring("M204"));
|
||||
}
|
||||
|
||||
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
|
||||
// they call the estimator directly. The estimate counts the filaments the print really uses,
|
||||
// so the two-filament shape gives the outer wall the second one.
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
|
||||
# Observe the geometry backend's actual CGAL layout without duplicating its flags.
|
||||
# This source is only added when the test suite is enabled.
|
||||
target_sources(libslic3r_cgal PRIVATE texture_import_cgal.cpp)
|
||||
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
${_TEST_NAME}_tests.cpp
|
||||
test_3mf.cpp
|
||||
@@ -31,13 +35,13 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_preset_diff.cpp
|
||||
test_vendor_cache.cpp
|
||||
test_preset_options.cpp
|
||||
test_edge_grid.cpp
|
||||
test_elephant_foot_compensation.cpp
|
||||
test_fill_corner_smoothing.cpp
|
||||
test_filament_mixer.cpp
|
||||
test_fill_plane_path.cpp
|
||||
test_adaptive_infill_field.cpp
|
||||
test_geometry.cpp
|
||||
test_kdtree.cpp
|
||||
test_imex_arrange.cpp
|
||||
test_imex_helpers.cpp
|
||||
test_imex_zones.cpp
|
||||
@@ -68,10 +72,12 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_wipe_path.cpp
|
||||
test_optimizers.cpp
|
||||
test_ordering_strategies.cpp
|
||||
test_shortest_path.cpp
|
||||
# test_png_io.cpp
|
||||
test_indexed_triangle_set.cpp
|
||||
test_connected_bodies.cpp
|
||||
test_texture_displacement.cpp
|
||||
test_texture_import.cpp
|
||||
test_instance_lock.cpp
|
||||
../libnest2d/printer_parts.cpp
|
||||
)
|
||||
|
||||
@@ -7,14 +7,11 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <numeric>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
@@ -303,90 +300,3 @@ TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") {
|
||||
REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested)));
|
||||
REQUIRE(top_level_expolygons(reference).size() == 1);
|
||||
}
|
||||
|
||||
// Rings flattened to x,y,x,y... and sorted, with each ring rotated to start at its lowest point: two
|
||||
// encodings of the same geometry compare equal however the pieces came back or wherever a ring started.
|
||||
static std::vector<std::vector<coord_t>> canonical_rings(const ExPolygons &expolygons)
|
||||
{
|
||||
std::vector<std::vector<coord_t>> rings;
|
||||
const auto add = [&rings](const Polygon &poly) {
|
||||
if (poly.points.empty())
|
||||
return;
|
||||
Points pts = poly.points;
|
||||
std::rotate(pts.begin(),
|
||||
std::min_element(pts.begin(), pts.end(), [](const Point &a, const Point &b) {
|
||||
return std::make_pair(a.x(), a.y()) < std::make_pair(b.x(), b.y());
|
||||
}),
|
||||
pts.end());
|
||||
std::vector<coord_t> flat;
|
||||
flat.reserve(pts.size() * 2);
|
||||
for (const Point &p : pts) {
|
||||
flat.emplace_back(p.x());
|
||||
flat.emplace_back(p.y());
|
||||
}
|
||||
rings.emplace_back(std::move(flat));
|
||||
};
|
||||
for (const ExPolygon &expoly : expolygons) {
|
||||
add(expoly.contour);
|
||||
for (const Polygon &hole : expoly.holes)
|
||||
add(hole);
|
||||
}
|
||||
std::sort(rings.begin(), rings.end());
|
||||
return rings;
|
||||
}
|
||||
|
||||
// The same rings, every coordinate within `tolerance`.
|
||||
static bool same_rings(const ExPolygons &a, const ExPolygons &b, coord_t tolerance)
|
||||
{
|
||||
const std::vector<std::vector<coord_t>> ra = canonical_rings(a), rb = canonical_rings(b);
|
||||
return std::equal(ra.begin(), ra.end(), rb.begin(), rb.end(), [tolerance](const std::vector<coord_t> &x, const std::vector<coord_t> &y) {
|
||||
return std::equal(x.begin(), x.end(), y.begin(), y.end(), [tolerance](coord_t u, coord_t v) { return std::abs(u - v) <= tolerance; });
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("Tiled diff and intersection return the same polygons as the plain calls", "[ClipperUtils]") {
|
||||
// A grid of disjoint framed squares, enough of them to be split into several tiles.
|
||||
const int n = 40;
|
||||
const coord_t cell = scaled<coord_t>(2.), side = scaled<coord_t>(1.5), frame = scaled<coord_t>(0.3);
|
||||
ExPolygons subject;
|
||||
for (int y = 0; y < n; ++ y)
|
||||
for (int x = 0; x < n; ++ x) {
|
||||
const Point o(x * cell, y * cell);
|
||||
ExPolygon square(Polygon({ o, o + Point(side, 0), o + Point(side, side), o + Point(0, side) }));
|
||||
Polygon hole({ o + Point(frame, frame), o + Point(frame, side - frame), o + Point(side - frame, side - frame), o + Point(side - frame, frame) });
|
||||
square.holes.emplace_back(std::move(hole));
|
||||
subject.emplace_back(std::move(square));
|
||||
}
|
||||
// Clip polygons crossing many squares, one of them large with holes of its own.
|
||||
Polygons clip;
|
||||
const coord_t span = n * cell;
|
||||
for (int i = 0; i < 8; ++ i) {
|
||||
const coord_t y0 = coord_t(i) * span / 8, y1 = y0 + scaled<coord_t>(0.9);
|
||||
clip.emplace_back(Polygon({ Point(- cell, y0), Point(span, y0 + cell * 3), Point(span, y1 + cell * 3), Point(- cell, y1) }));
|
||||
}
|
||||
ExPolygon big(Polygon({ Point(span / 4, span / 4), Point(3 * span / 4, span / 4), Point(3 * span / 4, 3 * span / 4), Point(span / 4, 3 * span / 4) }));
|
||||
for (int i = 0; i < 4; ++ i) {
|
||||
const Point o(span / 4 + scaled<coord_t>(3.1) + i * scaled<coord_t>(9.7), span / 4 + scaled<coord_t>(5.3));
|
||||
big.holes.emplace_back(Polygon({ o, o + Point(0, scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), 0) }));
|
||||
}
|
||||
polygons_append(clip, to_polygons(big));
|
||||
|
||||
const ApplySafetyOffset safety = GENERATE(ApplySafetyOffset::No, ApplySafetyOffset::Yes);
|
||||
|
||||
// The point of the fixture: below 128 pieces the helpers fall back to a single tile and the tiled
|
||||
// path under test is never taken.
|
||||
REQUIRE(ClipperUtils::tile_expolygons(subject, 32).size() > 1);
|
||||
|
||||
// With the safety offset a tile unites fewer clip polygons, so Clipper2 can round a crossing 1 unit differently.
|
||||
const coord_t tolerance = safety == ApplySafetyOffset::Yes ? 1 : 0;
|
||||
|
||||
const ExPolygons diff_plain = diff_ex(subject, clip, safety);
|
||||
const ExPolygons diff_tiled = diff_ex_by_piece(subject, clip, safety);
|
||||
REQUIRE(area(diff_plain) > 0.);
|
||||
CHECK(same_rings(diff_tiled, diff_plain, tolerance));
|
||||
|
||||
const ExPolygons intersection_plain = intersection_ex(subject, clip, safety);
|
||||
const ExPolygons intersection_tiled = intersection_ex_by_piece(subject, clip, safety);
|
||||
REQUIRE(area(intersection_plain) > 0.);
|
||||
CHECK(same_rings(intersection_tiled, intersection_plain, tolerance));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
#include "catch2/matchers/catch_matchers.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/EdgeGrid.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("EdgeGrid propagates signed distances across the interior and exterior", "[EdgeGrid][Regression]")
|
||||
{
|
||||
const Polygon rectangle {Point::new_scale(-10., -12.), Point::new_scale(10., -12.),
|
||||
Point::new_scale(10., 12.), Point::new_scale(-10., 12.)};
|
||||
const Polygons outlines {rectangle};
|
||||
const BoundingBox bounds(Point::new_scale(-30., -32.), Point::new_scale(30., 32.));
|
||||
EdgeGrid::Grid grid(bounds);
|
||||
grid.create(outlines, scale_(1.));
|
||||
// Distances beyond the initialized edge band require backward propagation.
|
||||
// With -fsanitize=shift, this also checks negative neighbour offsets.
|
||||
grid.calculate_sdf();
|
||||
const auto distance = [&](double x, double y) {
|
||||
return grid.signed_distance_bilinear(Point::new_scale(x, y)) * SCALING_FACTOR;
|
||||
};
|
||||
REQUIRE_THAT(distance(0., 0.), Catch::Matchers::WithinAbs(-10., 0.01));
|
||||
for (double sign : {-1., 1.}) {
|
||||
REQUIRE_THAT(distance(sign * 5., 0.), Catch::Matchers::WithinAbs(-5., 0.01));
|
||||
REQUIRE_THAT(distance(0., sign * 7.), Catch::Matchers::WithinAbs(-5., 0.01));
|
||||
REQUIRE_THAT(distance(sign * 20., 0.), Catch::Matchers::WithinAbs(10., 0.01));
|
||||
REQUIRE_THAT(distance(0., sign * 22.), Catch::Matchers::WithinAbs(10., 0.01));
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
|
||||
#include "libslic3r/KDTreeIndirect.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Visiting the nearby points gives what collecting them gives", "[KDTree]") {
|
||||
std::mt19937 rng(19937);
|
||||
std::uniform_real_distribution<float> coord(-50.f, 50.f);
|
||||
// Points in a box, so that a radius search returns anything from none of them to all of them.
|
||||
std::vector<Vec3f> points(2000);
|
||||
for (Vec3f &p : points)
|
||||
p = Vec3f(coord(rng), coord(rng), coord(rng));
|
||||
|
||||
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
|
||||
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
|
||||
std::vector<size_t> indices(points.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
tree.build(indices);
|
||||
|
||||
const float radius = GENERATE(0.5f, 5.f, 25.f, 200.f);
|
||||
for (int i = 0; i < 20; ++ i) {
|
||||
const Vec3f center(coord(rng), coord(rng), coord(rng));
|
||||
|
||||
const std::vector<size_t> collected = find_nearby_points(tree, center, radius);
|
||||
std::vector<size_t> visited;
|
||||
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
|
||||
|
||||
// Same points, and in the same order: a caller that keeps the first of several equally good ones
|
||||
// must get the same answer either way.
|
||||
REQUIRE(visited == collected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A radius search returns every point within the radius and no other", "[KDTree]") {
|
||||
std::mt19937 rng(2024);
|
||||
std::uniform_real_distribution<float> coord(-20.f, 20.f);
|
||||
std::vector<Vec3f> points(500);
|
||||
for (Vec3f &p : points)
|
||||
p = Vec3f(coord(rng), coord(rng), coord(rng));
|
||||
|
||||
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
|
||||
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
|
||||
std::vector<size_t> indices(points.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
tree.build(indices);
|
||||
|
||||
const Vec3f center(1.f, -2.f, 3.f);
|
||||
const float radius = 7.f;
|
||||
|
||||
std::vector<size_t> expected;
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
if ((points[i] - center).squaredNorm() < radius * radius)
|
||||
expected.emplace_back(i);
|
||||
|
||||
std::vector<size_t> visited;
|
||||
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
|
||||
std::sort(visited.begin(), visited.end());
|
||||
|
||||
REQUIRE(! expected.empty());
|
||||
REQUIRE(visited == expected);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
@@ -12,10 +13,13 @@
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/GCode/OrderingStrategies.hpp"
|
||||
#include "libslic3r/ShortestPath.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
#include <numeric>
|
||||
#include <random>
|
||||
#include <catch2/catch_approx.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -304,3 +308,60 @@ TEST_CASE("snake_core handles three points", "[Snake]") {
|
||||
|
||||
REQUIRE(is_permutation(p2, 3));
|
||||
}
|
||||
|
||||
// The shortest cycle through the points, trying every order.
|
||||
static double shortest_cycle_length(const Points& centers)
|
||||
{
|
||||
std::vector<size_t> path(centers.size());
|
||||
std::iota(path.begin(), path.end(), 0);
|
||||
double best = std::numeric_limits<double>::max();
|
||||
do {
|
||||
best = std::min(best, tsp_cycle_path_length(path, centers));
|
||||
} while (std::next_permutation(path.begin() + 1, path.end()));
|
||||
return best;
|
||||
}
|
||||
|
||||
static Points random_centers(unsigned seed, size_t count)
|
||||
{
|
||||
std::mt19937 rng(seed);
|
||||
std::uniform_real_distribution<> coordinate(0., 200.);
|
||||
Points centers;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
// One draw per statement, as the evaluation order of function arguments is unspecified.
|
||||
const double x = coordinate(rng);
|
||||
const double y = coordinate(rng);
|
||||
centers.emplace_back(Point::new_scale(x, y));
|
||||
}
|
||||
return centers;
|
||||
}
|
||||
|
||||
TEST_CASE("tsp_exact_cycle finds the shortest cycle", "[TSPPostProcessing]") {
|
||||
const size_t count = GENERATE(4, 5, 6, 8);
|
||||
for (unsigned seed = 1; seed <= 10; ++seed) {
|
||||
const Points centers = random_centers(seed, count);
|
||||
const std::vector<size_t> path = tsp_exact_cycle(centers);
|
||||
REQUIRE(is_permutation(path, count));
|
||||
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Default ordering tours up to ten points along the shortest cycle", "[TSPPostProcessing]") {
|
||||
const bool from_start = GENERATE(false, true);
|
||||
const Point start = Point::new_scale(-20., -20.);
|
||||
for (unsigned seed = 1; seed <= 5; ++seed) {
|
||||
const Points centers = random_centers(seed, 9);
|
||||
const std::vector<size_t> path = chain_points_with_postprocessing(centers, from_start ? &start : nullptr);
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Default ordering of many points is never longer than the greedy cycle", "[TSPPostProcessing]") {
|
||||
for (unsigned seed = 1; seed <= 10; ++seed) {
|
||||
const Points centers = random_centers(seed, 150);
|
||||
const std::vector<size_t> path = chain_points_with_postprocessing(centers, nullptr);
|
||||
REQUIRE(is_permutation(path, centers.size()));
|
||||
CHECK(euclidean_path_length(path, centers) <= euclidean_path_length(chain_points(centers, nullptr), centers) + SCALED_EPSILON);
|
||||
CHECK_FALSE(has_crossings(path, centers));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/ExtrusionEntityCollection.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/ShortestPath.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <numeric>
|
||||
#include <math.h>
|
||||
#include <random>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// Infill-like paths: lines of 1 to 30 mm scattered over a 100 mm square. The collection owns them, and entities hands
|
||||
// them out as the raw pointers chain_and_reorder_extrusion_entities() reorders.
|
||||
struct Paths
|
||||
{
|
||||
ExtrusionEntityCollection collection;
|
||||
std::vector<ExtrusionEntity*> entities;
|
||||
|
||||
Paths(unsigned seed, size_t count, bool can_reverse)
|
||||
{
|
||||
std::mt19937 rng(seed);
|
||||
std::uniform_real_distribution<> coordinate(0., 100.), length(1., 30.), angle(0., 2. * M_PI);
|
||||
Polylines lines;
|
||||
for (size_t i = 0; i < count; ++ i) {
|
||||
// One draw per statement, as the evaluation order of function arguments is unspecified.
|
||||
const double x = coordinate(rng);
|
||||
const double y = coordinate(rng);
|
||||
const double l = length(rng);
|
||||
const double t = angle(rng);
|
||||
const Vec2d a(x, y);
|
||||
const Vec2d b = a + l * Vec2d(std::cos(t), std::sin(t));
|
||||
lines.emplace_back(Point::new_scale(a), Point::new_scale(b));
|
||||
}
|
||||
extrusion_entities_append_paths(collection.entities, std::move(lines), erInternalInfill, 0.1, 0.45f, 0.2f, can_reverse);
|
||||
entities = collection.entities;
|
||||
}
|
||||
};
|
||||
|
||||
double travel(const std::vector<ExtrusionEntity*> &entities, const Point &start)
|
||||
{
|
||||
double length = 0.;
|
||||
Point last = start;
|
||||
for (const ExtrusionEntity *ee : entities) {
|
||||
length += (ee->first_point() - last).cast<double>().norm();
|
||||
last = ee->last_point();
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
// Travel of the chain that always goes on to the nearest free path end.
|
||||
double nearest_next_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
||||
{
|
||||
std::vector<char> taken(entities.size(), 0);
|
||||
double length = 0.;
|
||||
Point last = from;
|
||||
for (size_t step = 0; step < entities.size(); ++ step) {
|
||||
double best = std::numeric_limits<double>::max();
|
||||
size_t best_idx = 0;
|
||||
bool best_reversed = false;
|
||||
for (size_t i = 0; i < entities.size(); ++ i)
|
||||
for (const bool reversed : { false, true })
|
||||
if (! taken[i] && (can_reverse || ! reversed)) {
|
||||
const double d = ((reversed ? entities[i]->last_point() : entities[i]->first_point()) - last).cast<double>().norm();
|
||||
if (d < best) {
|
||||
best = d;
|
||||
best_idx = i;
|
||||
best_reversed = reversed;
|
||||
}
|
||||
}
|
||||
taken[best_idx] = 1;
|
||||
length += best;
|
||||
last = best_reversed ? entities[best_idx]->first_point() : entities[best_idx]->last_point();
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
// The shortest travel over every order and allowed reversal of the paths, from start.
|
||||
double shortest_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
||||
{
|
||||
std::vector<size_t> order(entities.size());
|
||||
std::iota(order.begin(), order.end(), 0);
|
||||
double best = std::numeric_limits<double>::max();
|
||||
do {
|
||||
for (size_t mask = 0; mask < (size_t(1) << order.size()) && (can_reverse || mask == 0); ++ mask) {
|
||||
double length = 0.;
|
||||
Point last = from;
|
||||
for (size_t k = 0; k < order.size(); ++ k) {
|
||||
const ExtrusionEntity *ee = entities[order[k]];
|
||||
const bool reversed = (mask >> k & 1) != 0;
|
||||
length += ((reversed ? ee->last_point() : ee->first_point()) - last).cast<double>().norm();
|
||||
last = reversed ? ee->first_point() : ee->last_point();
|
||||
}
|
||||
best = std::min(best, length);
|
||||
}
|
||||
} while (std::next_permutation(order.begin(), order.end()));
|
||||
return best;
|
||||
}
|
||||
|
||||
const Point start = Point::new_scale(-10., -10.);
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("An improved infill chain is never longer than the greedy or nearest-next one and keeps every path", "[ShortestPath]")
|
||||
{
|
||||
const size_t count = GENERATE(2, 3, 10, 60, 300);
|
||||
const bool can_reverse = GENERATE(true, false);
|
||||
double greedy_total = 0., improved_total = 0.;
|
||||
for (unsigned seed = 1; seed <= 20; ++ seed) {
|
||||
Paths greedy(seed, count, can_reverse), improved(seed, count, can_reverse);
|
||||
const double nearest_travel = nearest_next_travel(improved.entities, can_reverse, start);
|
||||
chain_and_reorder_extrusion_entities(greedy.entities, &start);
|
||||
chain_and_reorder_extrusion_entities(improved.entities, start);
|
||||
|
||||
std::vector<ExtrusionEntity*> sorted = improved.entities;
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
REQUIRE(std::unique(sorted.begin(), sorted.end()) == sorted.end());
|
||||
REQUIRE(sorted.size() == count);
|
||||
|
||||
const double greedy_travel = travel(greedy.entities, start), improved_travel = travel(improved.entities, start);
|
||||
CHECK(improved_travel <= greedy_travel + SCALED_EPSILON);
|
||||
CHECK(improved_travel <= nearest_travel + SCALED_EPSILON);
|
||||
greedy_total += greedy_travel;
|
||||
improved_total += improved_travel;
|
||||
}
|
||||
// Twenty random sets of ten or more paths always leave the greedy chain something to shorten.
|
||||
if (count >= 10)
|
||||
CHECK(improved_total < 0.95 * greedy_total);
|
||||
}
|
||||
|
||||
TEST_CASE("Improving an infill chain keeps the direction of paths that cannot reverse", "[ShortestPath]")
|
||||
{
|
||||
Paths paths(7, 100, false);
|
||||
std::map<const ExtrusionEntity*, Point> first_points;
|
||||
for (const ExtrusionEntity *ee : paths.entities)
|
||||
first_points.emplace(ee, ee->first_point());
|
||||
|
||||
chain_and_reorder_extrusion_entities(paths.entities, start);
|
||||
|
||||
for (const ExtrusionEntity *ee : paths.entities)
|
||||
CHECK(ee->first_point() == first_points.at(ee));
|
||||
}
|
||||
|
||||
TEST_CASE("Chains of a few paths are the shortest possible", "[ShortestPath]")
|
||||
{
|
||||
const size_t count = GENERATE(2, 3, 4, 5, 6);
|
||||
const bool can_reverse = GENERATE(true, false);
|
||||
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
||||
Paths paths(seed, count, can_reverse);
|
||||
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
||||
chain_and_reorder_extrusion_entities(paths.entities, start);
|
||||
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("The greedy chain of two to four paths is the shortest possible", "[ShortestPath]")
|
||||
{
|
||||
const size_t count = GENERATE(2, 3, 4);
|
||||
const bool can_reverse = GENERATE(true, false);
|
||||
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
||||
Paths paths(seed, count, can_reverse);
|
||||
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
||||
chain_and_reorder_extrusion_entities(paths.entities, &start);
|
||||
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A short chain need not start with the path nearest to its start", "[ShortestPath]")
|
||||
{
|
||||
// The nearest end leads 10 mm away from the two other paths, so the shortest chain leaves that path for last.
|
||||
const Point from = Point::new_scale(0., 0.);
|
||||
for (const bool greedy : { false, true }) {
|
||||
ExtrusionEntityCollection collection;
|
||||
extrusion_entities_append_paths(collection.entities,
|
||||
Polylines{ Polyline(Point::new_scale(-0.9, 0.), Point::new_scale(-10., 0.)), Polyline(Point::new_scale(1., 0.), Point::new_scale(2., 0.)),
|
||||
Polyline(Point::new_scale(3., 1.), Point::new_scale(4., 1.)) },
|
||||
erInternalInfill, 0.1, 0.45f, 0.2f);
|
||||
std::vector<ExtrusionEntity*> entities = collection.entities;
|
||||
const double shortest = shortest_travel(entities, true, from);
|
||||
if (greedy)
|
||||
chain_and_reorder_extrusion_entities(entities, &from);
|
||||
else
|
||||
chain_and_reorder_extrusion_entities(entities, from);
|
||||
CHECK(entities.back() == collection.entities.front());
|
||||
CHECK_THAT(travel(entities, from), Catch::Matchers::WithinAbs(shortest, SCALED_EPSILON));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TexturePainting.hpp"
|
||||
#include "texture_import_cgal.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void check_texture_import(const std::string& fixture)
|
||||
{
|
||||
const std::string filename = std::string(TEST_DATA_DIR) + "/texture_import/" + fixture;
|
||||
INFO(filename);
|
||||
const Slic3r::Model model = Slic3r::Model::read_from_file(filename);
|
||||
REQUIRE_FALSE(model.objects.empty());
|
||||
REQUIRE(model.texture_mesh);
|
||||
const Slic3r::TexturedMesh& mesh = *model.texture_mesh;
|
||||
|
||||
Slic3r::TexturePaintingSettings settings;
|
||||
settings.target_colors_num = 0; // Match the dialog's initial automatic color count.
|
||||
settings.smooth_weight = 0.5;
|
||||
settings.mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
|
||||
Slic3r::PaintedMesh painted;
|
||||
// Follow the dialog worker's format-dependent path, including the boundary
|
||||
// smoothing that needs a consistent CGAL Kd_tree layout across libraries.
|
||||
if (!mesh.precomputed_face_colors.empty())
|
||||
REQUIRE(Slic3r::face_colors_to_painting(mesh, painted, settings));
|
||||
else
|
||||
REQUIRE(Slic3r::texture_to_painting(mesh, painted, settings));
|
||||
|
||||
REQUIRE_FALSE(painted.indices.empty());
|
||||
REQUIRE(painted.face_colors.size() == painted.indices.size());
|
||||
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
|
||||
[](const auto& color) { return color[0] > color[2]; }));
|
||||
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
|
||||
[](const auto& color) { return color[2] > color[0]; }));
|
||||
|
||||
const std::vector<std::array<float, 4>> filament_colors{
|
||||
{1.f, 0.f, 0.f, 1.f}, {0.f, 0.f, 1.f, 1.f}};
|
||||
const auto matches = Slic3r::match_clusters_to_filaments(
|
||||
painted.cluster_colors, filament_colors, {"Red PLA", "Blue PLA"});
|
||||
REQUIRE(matches.size() == painted.cluster_colors.size());
|
||||
REQUIRE(std::any_of(matches.begin(), matches.end(),
|
||||
[](const auto& match) { return match.filament_index == 0; }));
|
||||
REQUIRE(std::any_of(matches.begin(), matches.end(),
|
||||
[](const auto& match) { return match.filament_index == 1; }));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Color-boundary smoothing shares its CGAL tree layout with the geometry backend", "[TextureImport][CGAL][Regression]")
|
||||
{
|
||||
// An ABI mismatch need not crash every link of the importer. Compare the
|
||||
// layouts directly so a lucky choice of inline symbols cannot mask it.
|
||||
REQUIRE(sizeof(TextureImportTest::SearchTree) == TextureImportTest::backend_search_tree_size());
|
||||
}
|
||||
|
||||
TEST_CASE("A JPEG textured GLB retains both colors through filament matching", "[TextureImport][Regression]")
|
||||
{
|
||||
check_texture_import("two_color_jpeg.glb");
|
||||
}
|
||||
|
||||
TEST_CASE("An OBJ preserves both MTL colors through filament matching", "[TextureImport][OBJ][Regression]")
|
||||
{
|
||||
check_texture_import("two_color_sphere/test_two_color_sphere.obj");
|
||||
}
|
||||
|
||||
TEST_CASE("An OBJ resolves its MTL and external JPEG through filament matching", "[TextureImport][OBJ][Regression]")
|
||||
{
|
||||
check_texture_import("two_color_sphere/test_two_color_sphere_textured.obj");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#include "texture_import_cgal.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
std::size_t TextureImportTest::backend_search_tree_size()
|
||||
{
|
||||
return sizeof(SearchTree);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <CGAL/AABB_face_graph_triangle_primitive.h>
|
||||
#include <CGAL/AABB_traits_3.h>
|
||||
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
|
||||
#include <CGAL/Kd_tree.h>
|
||||
#include <CGAL/Search_traits_adapter.h>
|
||||
#include <CGAL/Surface_mesh/Surface_mesh.h>
|
||||
#include <CGAL/property_map.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace TextureImportTest {
|
||||
|
||||
// The distance-query tree used by ColorUtils.cpp's boundary smoothing. Its
|
||||
// type name stays the same when CGAL changes its storage for TBB support.
|
||||
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
|
||||
using Mesh = CGAL::Surface_mesh<Kernel::Point_3>;
|
||||
using Primitive = CGAL::AABB_face_graph_triangle_primitive<Mesh>;
|
||||
using Traits = CGAL::AABB_traits_3<Kernel, Primitive>;
|
||||
using PointId = Traits::Point_and_primitive_id;
|
||||
using SearchTraits = CGAL::Search_traits_adapter<PointId, CGAL::First_of_pair_property_map<PointId>, Traits>;
|
||||
using SearchTree = CGAL::Kd_tree<SearchTraits>;
|
||||
|
||||
// Implemented inside the actual geometry target, using its private definitions.
|
||||
std::size_t backend_search_tree_size();
|
||||
|
||||
} // namespace TextureImportTest
|
||||
@@ -44,6 +44,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_shortcuts.cpp
|
||||
test_texture_color_palette.cpp
|
||||
test_texture_island_view.cpp
|
||||
test_timelapse_codecs.cpp
|
||||
test_file_url.cpp
|
||||
test_imgui_wrapper.cpp
|
||||
test_user_manager.cpp
|
||||
|
||||
@@ -81,3 +81,24 @@ TEST_CASE("Payload rewrite covers nested trays, calibration lists and mapping in
|
||||
CHECK(BBLPrinterAgent::from_orca_payload("not json") == "not json");
|
||||
CHECK(BBLPrinterAgent::from_orca_payload(R"({"print":{"command":"pushall"}})") == R"({"print":{"command":"pushall"}})");
|
||||
}
|
||||
|
||||
TEST_CASE("Payload rewrite covers the nozzle rack status and the auto nozzle mapping request", "[BambuFilamentIds]")
|
||||
{
|
||||
const ScopedRepoResourcesDir repo_resources;
|
||||
const std::string petg_hf = orca_id_of("GFG96");
|
||||
|
||||
const std::string rack_status =
|
||||
R"({"print":{"device":{"nozzle":{"info":[{"id":16,"fila_id":"GFG96","color_m":"FFFFFFFF"},{"id":17,"fila_id":"P22f54c9"}]}}}})";
|
||||
json inbound = json::parse(BBLPrinterAgent::to_orca_payload(rack_status));
|
||||
CHECK(inbound["print"]["device"]["nozzle"]["info"][0]["fila_id"] == petg_hf);
|
||||
CHECK(inbound["print"]["device"]["nozzle"]["info"][1]["fila_id"] == "P22f54c9"); // user id: untouched
|
||||
|
||||
const std::string request = R"({"print":{"command":"get_auto_nozzle_mapping","ams_mapping":[65535,65024],)"
|
||||
R"("fila_info":[{"id":2,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"}],)"
|
||||
R"("nozzle_info":[{"pos":16,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"},{"pos":1,"cate":""}]}})";
|
||||
json outbound = json::parse(BBLPrinterAgent::from_orca_payload(request));
|
||||
CHECK(outbound["print"]["fila_info"][0]["cate"] == "GFG96");
|
||||
CHECK(outbound["print"]["nozzle_info"][0]["cate"] == "GFG96");
|
||||
CHECK(outbound["print"]["nozzle_info"][1]["cate"] == ""); // empty nozzle
|
||||
CHECK(outbound["print"]["ams_mapping"] == json::parse("[65535,65024]"));
|
||||
}
|
||||
@@ -420,12 +420,13 @@ TEST_CASE("Moonraker discovery waits for a complete device identity", "[Moonrake
|
||||
agent.device_info.dev_id = "127.0.0.1:7125";
|
||||
agent.device_info.dev_ip = agent.device_info.dev_id;
|
||||
agent.device_info.base_url = "http://127.0.0.1:7125";
|
||||
agent.device_info.model_name = "Test model";
|
||||
agent.start_discovery(true, false);
|
||||
REQUIRE(announcements == 1);
|
||||
REQUIRE(agent.fetch_count == 1);
|
||||
REQUIRE(agent.fetch_count == 0);
|
||||
REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id);
|
||||
REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip);
|
||||
REQUIRE(announcement.at("dev_name") == "Test printer");
|
||||
REQUIRE(announcement.at("dev_name") == "Test model");
|
||||
}
|
||||
|
||||
TEST_CASE("Stopping the status stream gives a silent printer no more than its timeouts", "[MoonrakerPrinterAgent][StatusStream]")
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <boost/asio/write.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
@@ -91,7 +92,9 @@ using boost::asio::ip::tcp;
|
||||
class LoopbackServer
|
||||
{
|
||||
public:
|
||||
enum class Mode { Ok, Unavailable, Silent };
|
||||
// Session answers every request with the body given to set_session_body(), as the token endpoint
|
||||
// does, after 200 ms so the client is still waiting when the test makes its next call.
|
||||
enum class Mode { Ok, Unavailable, Silent, Session };
|
||||
|
||||
struct Request
|
||||
{
|
||||
@@ -113,6 +116,11 @@ public:
|
||||
|
||||
std::string url() const { return "http://127.0.0.1:" + std::to_string(m_acceptor.local_endpoint().port()); }
|
||||
void set_mode(Mode mode) { m_mode = mode; }
|
||||
void set_session_body(std::string body)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_session_body = std::move(body);
|
||||
}
|
||||
|
||||
bool wait_for_accepts(size_t count, std::chrono::milliseconds timeout)
|
||||
{
|
||||
@@ -172,13 +180,31 @@ private:
|
||||
const Mode mode = m_mode;
|
||||
if (mode == Mode::Silent)
|
||||
return;
|
||||
const std::string_view response = mode == Mode::Ok ?
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" :
|
||||
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
|
||||
boost::asio::async_write(connection->socket, boost::asio::buffer(response),
|
||||
[connection](boost::system::error_code, size_t) {
|
||||
boost::system::error_code ignored;
|
||||
connection->socket.shutdown(tcp::socket::shutdown_both, ignored);
|
||||
auto response = std::make_shared<std::string>();
|
||||
if (mode == Mode::Session) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
*response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " +
|
||||
std::to_string(m_session_body.size()) + "\r\nConnection: close\r\n\r\n" + m_session_body;
|
||||
} else {
|
||||
*response = mode == Mode::Ok ?
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" :
|
||||
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
|
||||
}
|
||||
auto write = [connection, response] {
|
||||
boost::asio::async_write(connection->socket, boost::asio::buffer(*response),
|
||||
[connection, response](boost::system::error_code, size_t) {
|
||||
boost::system::error_code ignored;
|
||||
connection->socket.shutdown(tcp::socket::shutdown_both, ignored);
|
||||
});
|
||||
};
|
||||
if (mode != Mode::Session) {
|
||||
write();
|
||||
return;
|
||||
}
|
||||
auto timer = std::make_shared<boost::asio::steady_timer>(m_io, 200ms);
|
||||
timer->async_wait([timer, write](boost::system::error_code ec) {
|
||||
if (!ec)
|
||||
write();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -192,8 +218,12 @@ private:
|
||||
std::condition_variable m_cv;
|
||||
std::vector<Request> m_requests;
|
||||
std::vector<std::shared_ptr<Connection>> m_connections;
|
||||
std::string m_session_body;
|
||||
};
|
||||
|
||||
// A JWT whose only claim is {"exp":4102444800} (2100-01-01); the agent reads nothing else from it.
|
||||
constexpr const char* FAR_FUTURE_JWT = "eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.sig";
|
||||
|
||||
// What the agent reported through its callbacks, recorded on whichever thread delivers it so the
|
||||
// assertions can run on the test thread.
|
||||
struct CallbackLog
|
||||
@@ -515,3 +545,24 @@ TEST_CASE_METHOD(HealthCheckFixture, "The health check does no token work", "[Or
|
||||
CHECK(agent->is_user_login());
|
||||
CHECK(agent->get_access_token() == "test-token");
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(HealthCheckFixture, "A token request made while start() refreshes waits for it instead of refreshing again", "[OrcaCloudServiceAgent]")
|
||||
{
|
||||
// A session saved by a previous run: start() refreshes it on its own thread.
|
||||
agent->persist_user_secret(nlohmann::json{{"refresh_token", "stored-refresh-token"}, {"user_id", "test-user-id"}}.dump());
|
||||
const std::string access_token = FAR_FUTURE_JWT;
|
||||
server.set_session_body(nlohmann::json{{"access_token", access_token},
|
||||
{"refresh_token", "rotated-refresh-token"},
|
||||
{"user_id", "test-user-id"}}.dump());
|
||||
server.set_mode(LoopbackServer::Mode::Session);
|
||||
|
||||
REQUIRE(agent->start() == BAMBU_NETWORK_SUCCESS);
|
||||
CHECK(agent->ensure_token_fresh("test"));
|
||||
CHECK(agent->get_access_token() == access_token);
|
||||
|
||||
size_t token_posts = 0;
|
||||
for (const auto& request : server.requests())
|
||||
if (boost::starts_with(request.request_line, "POST /auth/v1/token"))
|
||||
++token_posts;
|
||||
CHECK(token_posts == 1);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include "libslic3r/GCode/ThumbnailData.hpp"
|
||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/TexturePainting.hpp"
|
||||
#include "slic3r/GUI/AVVideoDecoder.hpp"
|
||||
#include "slic3r/GUI/Printer/BambuTunnel.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/init.h>
|
||||
#include <wx/mstream.h>
|
||||
|
||||
TEST_CASE("JPEG texture decoding coexists with thumbnails and video frames", "[TimelapseCodecs][Regression]")
|
||||
{
|
||||
#if defined(__linux__)
|
||||
if (!std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY"))
|
||||
SKIP("This integration test needs a display (for example, xvfb-run).");
|
||||
#endif
|
||||
wxInitializer init;
|
||||
REQUIRE(init.IsOk());
|
||||
wxInitAllImageHandlers();
|
||||
|
||||
// Generate a JPEG through the same encoder used for G-code thumbnails.
|
||||
constexpr int width = 160, height = 90, iterations = 1000;
|
||||
Slic3r::ThumbnailData thumbnail;
|
||||
thumbnail.set(width, height);
|
||||
for (std::size_t i = 0; i < thumbnail.pixels.size(); i += 4) {
|
||||
thumbnail.pixels[i] = 180;
|
||||
thumbnail.pixels[i + 1] = 70;
|
||||
thumbnail.pixels[i + 2] = 30;
|
||||
thumbnail.pixels[i + 3] = 255;
|
||||
}
|
||||
const auto jpeg = Slic3r::GCodeThumbnails::compress_thumbnail(thumbnail, Slic3r::GCodeThumbnailsFormat::JPG);
|
||||
REQUIRE(jpeg);
|
||||
REQUIRE(jpeg->size > 0);
|
||||
const auto* bytes = static_cast<const unsigned char*>(jpeg->data);
|
||||
Slic3r::TextureImage texture;
|
||||
texture.width = texture.height = -1;
|
||||
texture.data.assign(bytes, bytes + jpeg->size);
|
||||
|
||||
std::atomic<bool> texture_ok{true};
|
||||
std::thread texture_worker([&] {
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
std::vector<unsigned char> pixels;
|
||||
int decoded_width = 0, decoded_height = 0;
|
||||
if (!Slic3r::decode_texture_to_pixels(texture, pixels, decoded_width, decoded_height) ||
|
||||
decoded_width != width || decoded_height != height || pixels.size() != width * height * 3)
|
||||
texture_ok = false;
|
||||
}
|
||||
});
|
||||
|
||||
// PrinterFileSystem decodes timelapse thumbnails with wxImage. Do not
|
||||
// assert until the worker is joined: Catch assertions can unwind the scope.
|
||||
bool thumbnails_ok = true;
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
wxMemoryInputStream stream(jpeg->data, jpeg->size);
|
||||
wxImage image(stream, "image/jpeg");
|
||||
if (!image.IsOk() || image.GetWidth() != width || image.GetHeight() != height ||
|
||||
image.GetRed(width / 2, height / 2) < 170 || image.GetGreen(width / 2, height / 2) > 80)
|
||||
thumbnails_ok = false;
|
||||
}
|
||||
texture_worker.join();
|
||||
REQUIRE(texture_ok.load());
|
||||
REQUIRE(thumbnails_ok);
|
||||
|
||||
// Exercise Orca's real FFmpeg decoder and its platform display conversion.
|
||||
AVVideoDecoder decoder;
|
||||
Bambu_StreamInfo info{};
|
||||
info.type = VIDE;
|
||||
info.sub_type = MJPG;
|
||||
info.format_type = video_jpeg;
|
||||
REQUIRE(decoder.open(info) == 0);
|
||||
Bambu_Sample sample{};
|
||||
sample.size = static_cast<int>(jpeg->size);
|
||||
sample.buffer = bytes;
|
||||
for (int i = 0; i < iterations; ++i) {
|
||||
REQUIRE(decoder.decode(sample) == 0);
|
||||
wxImage image;
|
||||
REQUIRE(decoder.toWxImage(image, wxSize(width, height)));
|
||||
REQUIRE(image.IsOk());
|
||||
REQUIRE(image.GetWidth() == width);
|
||||
REQUIRE(image.GetHeight() == height);
|
||||
#ifdef _WIN32
|
||||
// Playback uses this raw 32-bit bitmap conversion only on Windows.
|
||||
wxBitmap bitmap;
|
||||
REQUIRE(decoder.toWxBitmap(bitmap, wxSize(width, height)));
|
||||
REQUIRE(bitmap.IsOk());
|
||||
REQUIRE(bitmap.GetWidth() == width);
|
||||
REQUIRE(bitmap.GetHeight() == height);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user