diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml
index ff07d1efb7..1918c009db 100644
--- a/.github/workflows/build_all.yml
+++ b/.github/workflows/build_all.yml
@@ -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
diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml
index 60456ad171..e9c4603bc8 100644
--- a/.github/workflows/build_orca.yml
+++ b/.github/workflows/build_orca.yml
@@ -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
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 06ea0dbd6f..62798ebfc4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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)
diff --git a/build_linux.sh b/build_linux.sh
index cf07d7e9df..b4c03e52ec 100755
--- a/build_linux.sh
+++ b/build_linux.sh
@@ -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"
diff --git a/build_release_macos.sh b/build_release_macos.sh
index cdc5dbbe3c..9eca2e7f40 100755
--- a/build_release_macos.sh
+++ b/build_release_macos.sh
@@ -47,7 +47,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
u )
export BUILD_TARGET="universal"
;;
- h ) echo "Usage: ./build_release_macos.sh [-d]"
+ h ) echo "Usage: ./build_release_macos.sh [-d] [-- CMAKE_ARGS...]"
echo " -d: Build deps only"
echo " -a: Set ARCHITECTURE (arm64 or x86_64 or universal)"
echo " -s: Build slicer only"
@@ -60,13 +60,33 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
+ echo " -- CMAKE_ARGS...: pass everything after '--' to the cmake configure step of the deps (-d) and slicer (-s) builds,"
+ echo " e.g. -DDESTDIR=
for the deps, -DCMAKE_PREFIX_PATH=/usr/local -DSLIC3R_PCH=OFF for the slicer, --fresh."
+ echo " They come last on the cmake command line. Use absolute paths: cmake runs in the build directory."
exit 0
;;
+ : )
+ echo "Option -${OPTARG} requires an argument, aborting."
+ exit 1
+ ;;
* )
+ echo "Unknown option -${OPTARG}, aborting. CMake arguments must follow '--'."
+ exit 1
;;
esac
done
+# getopts stops at '--'; whatever follows is handed to cmake unchanged.
+if [[ ${OPTIND} -le $# ]] ; then
+ separator_index=$((OPTIND-1))
+ if [[ "${!separator_index}" != "--" ]] ; then
+ echo "Unexpected argument '${!OPTIND}': cmake arguments must follow '--', aborting."
+ exit 1
+ fi
+fi
+shift $((OPTIND-1))
+EXTRA_CMAKE_ARGS=("$@")
+
# Set defaults
if [ -z "$ARCH" ]; then
@@ -211,7 +231,8 @@ function build_deps() {
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
- ${CMAKE_POLICY_COMPAT}
+ ${CMAKE_POLICY_COMPAT} \
+ "${EXTRA_CMAKE_ARGS[@]}"
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
)
@@ -314,7 +335,8 @@ function build_slicer() {
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
- ${CMAKE_POLICY_COMPAT}
+ ${CMAKE_POLICY_COMPAT} \
+ "${EXTRA_CMAKE_ARGS[@]}"
fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
)
diff --git a/build_win.bat b/build_win.bat
index dc19df06bc..8af3581abf 100644
--- a/build_win.bat
+++ b/build_win.bat
@@ -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"
)
diff --git a/deps/OpenCV/OpenCV.cmake b/deps/OpenCV/OpenCV.cmake
index 0290b3f47a..d465e6129d 100644
--- a/deps/OpenCV/OpenCV.cmake
+++ b/deps/OpenCV/OpenCV.cmake
@@ -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
diff --git a/deps/wxWidgets/wxWidgets.cmake b/deps/wxWidgets/wxWidgets.cmake
index e57e82f3e9..605fad2f0b 100644
--- a/deps/wxWidgets/wxWidgets.cmake
+++ b/deps/wxWidgets/wxWidgets.cmake
@@ -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
diff --git a/docs/HLSD/filament_id.md b/docs/HLSD/filament_id.md
index 122dfb6fdb..555a5d6ad5 100644
--- a/docs/HLSD/filament_id.md
+++ b/docs/HLSD/filament_id.md
@@ -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 |
diff --git a/docs/HLSD/travel-optimization.md b/docs/HLSD/travel-optimization.md
new file mode 100644
index 0000000000..119133cee2
--- /dev/null
+++ b/docs/HLSD/travel-optimization.md
@@ -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.
diff --git a/localization/i18n/ru/OrcaSlicer_ru.po b/localization/i18n/ru/OrcaSlicer_ru.po
index 5c7127dbc6..639d9948fc 100644
--- a/localization/i18n/ru/OrcaSlicer_ru.po
+++ b/localization/i18n/ru/OrcaSlicer_ru.po
@@ -36839,7 +36839,9 @@ msgstr ""
msgid ""
"Reverse on even\n"
"Did you know that Reverse on even feature can significantly improve the surface quality of your overhangs? However, it can cause wall inconsistencies so use carefully!"
-msgstr "Реверс на чётных слояхСовет: настройка Реверс на чётных слоях может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью."
+msgstr ""
+"Реверс на чётных слоях\n"
+"Совет: настройка Реверс на чётных слоях может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью."
#: resources/data/hints.ini: [hint:Cut Tool]
msgid ""
diff --git a/resources/fonts/NotoSansKR-Bold.ttf b/resources/fonts/NotoSansKR-Bold.ttf
deleted file mode 100644
index 6cf639eb7d..0000000000
Binary files a/resources/fonts/NotoSansKR-Bold.ttf and /dev/null differ
diff --git a/resources/fonts/NotoSansKR-Regular.ttf b/resources/fonts/NotoSansKR-Regular.ttf
deleted file mode 100644
index 1b14d32473..0000000000
Binary files a/resources/fonts/NotoSansKR-Regular.ttf and /dev/null differ
diff --git a/scripts/check_exception_throw_info.ps1 b/scripts/check_exception_throw_info.ps1
new file mode 100644
index 0000000000..5fd76599b8
--- /dev/null
+++ b/scripts/check_exception_throw_info.ps1
@@ -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@@@8) and one with it
+ (_CT??_R0?AV@@@8??0...). 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\
+ 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 \src\, 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
diff --git a/scripts/test_build_win.ps1 b/scripts/test_build_win.ps1
index 5f36fb7ac9..0c2a26c952 100644
--- a/scripts/test_build_win.ps1
+++ b/scripts/test_build_win.ps1
@@ -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')
diff --git a/scripts/test_check_exception_throw_info.ps1 b/scripts/test_check_exception_throw_info.ps1
new file mode 100644
index 0000000000..0c2ef2edfb
--- /dev/null
+++ b/scripts/test_check_exception_throw_info.ps1
@@ -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)
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index 265efb2eea..4acea143cc 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -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 $<$:SLIC3R_CONSOLE_LOG>)
endif()
diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp
index 9d76262ec6..9f70498499 100644
--- a/src/libslic3r/Config.hpp
+++ b/src/libslic3r/Config.hpp
@@ -377,7 +377,8 @@ public:
virtual void set(const ConfigOption* rhs, size_t start, size_t len) = 0;
virtual void set_with_restore(const ConfigOptionVectorBase* rhs, std::vector& restore_index, int stride) = 0;
virtual void set_with_restore_2(const ConfigOptionVectorBase* rhs, std::vector& restore_index, int start, int len, bool skip_error = false) = 0;
- virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector& diff_index, int stride) = 0;
+ // keep_nil: copy a nil column from rhs as well, for options whose nil means "disabled" rather than "inherit".
+ virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector& diff_index, int stride, bool keep_nil = false) = 0;
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector& dest_index, int stride) = 0;
virtual void set_with_nil(const ConfigOptionVectorBase* rhs, const ConfigOptionVectorBase* inherits, int stride) = 0;
// Resize the vector of values, copy the newly added values from opt_default if provided.
@@ -580,7 +581,7 @@ public:
//set a item related with extruder variants when loading user config, only set the different value of some extruder
//rhs: item from user config
//diff_index: which index in this vector need to be set
- virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector& diff_index, int stride) override
+ virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector& diff_index, int stride, bool keep_nil) override
{
if (rhs->type() == this->type()) {
// Assign the first value of the rhs vector.
@@ -592,7 +593,7 @@ public:
for (size_t i = 0; i < diff_index.size(); i++) {
if (diff_index[i] != -1) {
for (size_t j = 0; j < stride; j++) {
- if (!other->is_nil(diff_index[i] * stride))
+ if (keep_nil || !other->is_nil(diff_index[i] * stride))
this->values[i * stride + j] = other->values[diff_index[i] * stride + j];
}
}
@@ -841,8 +842,12 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
- iss >> this->value;
- return !iss.fail();
+ double value;
+ iss >> value;
+ if (iss.fail())
+ return false;
+ this->value = value;
+ return true;
}
bool operator==(const ConfigOption &rhs) const override
@@ -1033,8 +1038,12 @@ public:
{
UNUSED(append);
std::istringstream iss(str);
- iss >> this->value;
- return !iss.fail();
+ int value;
+ iss >> value;
+ if (iss.fail())
+ return false;
+ this->value = value;
+ return true;
}
ConfigOptionInt& operator=(const ConfigOption *opt)
@@ -1252,8 +1261,12 @@ public:
UNUSED(append);
// don't try to parse the trailing % since it's optional
std::istringstream iss(str);
- iss >> this->value;
- return !iss.fail();
+ double value;
+ iss >> value;
+ if (iss.fail())
+ return false;
+ this->value = value;
+ return true;
}
private:
@@ -1365,10 +1378,14 @@ public:
bool deserialize(const std::string &str, bool append = false) override
{
UNUSED(append);
- this->percent = str.find_first_of("%") != std::string::npos;
std::istringstream iss(str);
- iss >> this->value;
- return !iss.fail();
+ double value;
+ iss >> value;
+ if (iss.fail())
+ return false;
+ this->value = value;
+ this->percent = str.find_first_of("%") != std::string::npos;
+ return true;
}
private:
@@ -1538,8 +1555,12 @@ public:
{
UNUSED(append);
char dummy;
- return sscanf(str.data(), " %lf , %lf %c", &this->value(0), &this->value(1), &dummy) == 2 ||
- sscanf(str.data(), " %lf x %lf %c", &this->value(0), &this->value(1), &dummy) == 2;
+ Vec2d value;
+ if (sscanf(str.data(), " %lf , %lf %c", &value(0), &value(1), &dummy) != 2 &&
+ sscanf(str.data(), " %lf x %lf %c", &value(0), &value(1), &dummy) != 2)
+ return false;
+ this->value = value;
+ return true;
}
private:
@@ -1658,8 +1679,12 @@ public:
{
UNUSED(append);
char dummy;
- return sscanf(str.data(), " %lf , %lf , %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3 ||
- sscanf(str.data(), " %lf x %lf x %lf %c", &this->value(0), &this->value(1), &this->value(2), &dummy) == 3;
+ Vec3d value;
+ if (sscanf(str.data(), " %lf , %lf , %lf %c", &value(0), &value(1), &value(2), &dummy) != 3 &&
+ sscanf(str.data(), " %lf x %lf x %lf %c", &value(0), &value(1), &value(2), &dummy) != 3)
+ return false;
+ this->value = value;
+ return true;
}
private:
diff --git a/src/libslic3r/EdgeGrid.cpp b/src/libslic3r/EdgeGrid.cpp
index c6e003505a..35d3608dd8 100644
--- a/src/libslic3r/EdgeGrid.cpp
+++ b/src/libslic3r/EdgeGrid.cpp
@@ -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
diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp
index a852f2c42b..63ebff49c2 100644
--- a/src/libslic3r/Fill/FillBase.cpp
+++ b/src/libslic3r/Fill/FillBase.cpp
@@ -11,6 +11,10 @@
#include
#include
#include
+#include
+#include
+#include
+#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 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 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 &order) {
+ std::vector 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 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::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::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 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::vectorpoint_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::vectorpoint_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::vectorprev_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::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::max() || l > anchor_length_max)
+ if (l == std::numeric_limits::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::vectorpoint_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 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();
+ const Vec2d to_copy = (contour[other->point_idx] - end).cast();
+ 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> 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 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();
+ 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> 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().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 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 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 lines(std::move(near_lines));
+ auto crosses = [&](size_t a, size_t b) {
+ const Vec2d p = end_point(a).cast(), q = end_point(b).cast();
+ 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()), Point((q - SCALED_EPSILON * dir).cast()));
+ if (! lines.get_lines().empty() && ! lines.intersections_with_line(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 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
diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp
index d1f07af77c..f60fde6d1d 100644
--- a/src/libslic3r/Fill/FillBase.hpp
+++ b/src/libslic3r/Fill/FillBase.hpp
@@ -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_
diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp
index 1500483f6e..88fef3bb35 100644
--- a/src/libslic3r/GCode.cpp
+++ b/src/libslic3r/GCode.cpp
@@ -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 {
diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp
index 6346651448..3512df6ab8 100644
--- a/src/libslic3r/GCode/GCodeProcessor.cpp
+++ b/src/libslic3r/GCode/GCodeProcessor.cpp
@@ -24,6 +24,7 @@
#include
#include
+#include
#include
#include
#include
@@ -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();
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(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(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(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(mode)].time) : 0.0f;
+ return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast(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 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)))
+ g1_axes[axis] = line.precise_value(static_cast(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, 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, 4>& axes
}
}
+ if (m_flavor == gcfKlipper && !is_extrusion_only_move(delta_pos)) {
+ const float max_velocity = get_axis_max_feedrate(static_cast(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, 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(i), static_cast(a), m_machine_config_idx);
if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]);
}
@@ -5277,20 +5383,42 @@ void GCodeProcessor::process_G1(const std::array, 4>& axes
}
// calculates block acceleration
- float acceleration =
- (type == EMoveType::Travel) ? get_travel_acceleration(static_cast(i)) :
- (is_extrusion_only_move(delta_pos) ?
- get_retract_acceleration(static_cast(i)) :
- get_acceleration(static_cast(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(i), E, m_machine_config_idx);
+ if (acceleration <= 0.0f)
+ acceleration = get_retract_acceleration(static_cast(i));
+ } else
+ acceleration =
+ (m_flavor == gcfKlipper) ? get_acceleration(static_cast(i)) :
+ (type == EMoveType::Travel) ? get_travel_acceleration(static_cast(i)) :
+ (is_extrusion_only_move(delta_pos) ?
+ get_retract_acceleration(static_cast(i)) :
+ get_acceleration(static_cast(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(i), static_cast(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(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, 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, 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(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
if (static_cast(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> 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(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(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
- set_acceleration(static_cast(i), _accl);
- set_travel_acceleration(static_cast(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(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(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(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 actual_speed_moves;
for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
TimeMachine& machine = m_time_processor.machines[i];
+ machine.klipper = m_flavor == gcfKlipper;
machine.calculate_time(m_result, static_cast(i), keep_last_n_blocks, additional_time, target_move_type, is_final);
if (static_cast(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;
diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp
index 72fb84bad0..b1fc911104 100644
--- a/src/libslic3r/GCode/GCodeProcessor.hpp
+++ b/src/libslic3r/GCode/GCodeProcessor.hpp
@@ -91,12 +91,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
struct Mode
{
- float time;
+ double time;
float prepare_time;
std::vector>> 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>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const;
diff --git a/src/libslic3r/GCode/OrderingStrategies.cpp b/src/libslic3r/GCode/OrderingStrategies.cpp
index 31b80e2697..4ac7ce6cb6 100644
--- a/src/libslic3r/GCode/OrderingStrategies.cpp
+++ b/src/libslic3r/GCode/OrderingStrategies.cpp
@@ -9,6 +9,7 @@
#include "libslic3r/libslic3r.h"
#include
+#include
#include
#include
#include
@@ -226,6 +227,41 @@ bool tsp_remove_crossings(std::vector& path, const Points& centers)
return improved;
}
+std::vector tsp_exact_cycle(const Points& centers)
+{
+ const size_t n = centers.size();
+ std::vector 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 cost((size_t(1) << m) * m, std::numeric_limits::max());
+ std::vector from(cost.size(), size_t(-1));
+ auto dist = [¢ers](size_t a, size_t b) { return (centers[a].cast() - centers[b].cast()).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::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& path, const Points& centers)
{
size_t pn = path.size();
diff --git a/src/libslic3r/GCode/OrderingStrategies.hpp b/src/libslic3r/GCode/OrderingStrategies.hpp
index a42a338a52..95a6e77bcf 100644
--- a/src/libslic3r/GCode/OrderingStrategies.hpp
+++ b/src/libslic3r/GCode/OrderingStrategies.hpp
@@ -29,6 +29,9 @@ bool tsp_2opt_improve(std::vector& path, const Points& centers, int max_
// Returns true if any crossing was removed.
bool tsp_remove_crossings(std::vector& 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 tsp_exact_cycle(const Points& centers);
+
// Rotate the cycle so the closing edge (last -> first) is minimized.
void tsp_rotate_minimize_closing(std::vector& path, const Points& centers);
diff --git a/src/libslic3r/GCode/WipeTower.cpp b/src/libslic3r/GCode/WipeTower.cpp
index b2ddc75a34..e081717ecf 100644
--- a/src/libslic3r/GCode/WipeTower.cpp
+++ b/src/libslic3r/GCode/WipeTower.cpp
@@ -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
diff --git a/src/libslic3r/GCodeReader.cpp b/src/libslic3r/GCodeReader.cpp
index eff557d817..1b545f86b5 100644
--- a/src/libslic3r/GCodeReader.cpp
+++ b/src/libslic3r/GCodeReader.cpp
@@ -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
diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp
index e65dff193c..97699a99e1 100644
--- a/src/libslic3r/GCodeReader.hpp
+++ b/src/libslic3r/GCodeReader.hpp
@@ -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;
};
diff --git a/src/libslic3r/GCodeWriter.cpp b/src/libslic3r/GCodeWriter.cpp
index 62382f8214..805e6e2f52 100644
--- a/src/libslic3r/GCodeWriter.cpp
+++ b/src/libslic3r/GCodeWriter.cpp
@@ -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) {
diff --git a/src/libslic3r/GCodeWriter.hpp b/src/libslic3r/GCodeWriter.hpp
index 3c90a4e9e1..fcbd68b282 100644
--- a/src/libslic3r/GCodeWriter.hpp
+++ b/src/libslic3r/GCodeWriter.hpp
@@ -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;
diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index edd93390b4..81b78ec092 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -797,7 +797,7 @@ bool Preset::save(DynamicPrintConfig* parent_config)
{
ConfigOption *opt_src = config.option(option);
ConfigOption *opt_dst = temp_config.option(option, true);
- if (opt_dst->is_scalar() || !(opt_dst->nullable()))
+ if (opt_dst->is_scalar() || !(opt_dst->nullable()) || is_filament_extruder_override_key(option))
opt_dst->set(opt_src);
else {
ConfigOptionVectorBase* opt_vec_src = static_cast(opt_src);
@@ -2132,7 +2132,7 @@ Preset* PresetCollection::get_preset_differed_for_save(Preset& preset)
{
ConfigOption *opt_src = preset.config.option(option);
ConfigOption *opt_dst = temp_config.option(option, true);
- if (opt_dst->is_scalar() || !(opt_dst->nullable()))
+ if (opt_dst->is_scalar() || !(opt_dst->nullable()) || is_filament_extruder_override_key(option))
opt_dst->set(opt_src);
else {
ConfigOptionVectorBase* opt_vec_src = static_cast(opt_src);
@@ -2199,9 +2199,12 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map 100%), silently
// resurrecting an override the user turned off. See GitHub issue on Retract Before Wipe.
if (m_type == Preset::TYPE_FILAMENT) {
- for (const std::string& opt_key : filament_extruder_override_keys) {
- if (key_values.count(opt_key))
- continue; // already carried by the diff
+ for (const std::string& opt_key : preset.config.keys()) {
+ if (!is_filament_extruder_override_key(opt_key) || key_values.count(opt_key))
+ continue; // not an override, or already carried by the diff
+ const ConfigOptionDef *def = print_config_def.get(opt_key);
+ if (def == nullptr || !def->default_value || def->default_value->is_nil())
+ continue; // the option's default is nil too, so an absent key cannot come back as a value
const auto* opt_vec = dynamic_cast(preset.config.option(opt_key));
if (opt_vec == nullptr)
continue;
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index 48d92da201..212d8d8cb5 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -119,12 +119,18 @@ const std::vector filament_extruder_override_keys = {
};
// Some filament override parameters are generated from filament_extruder_override_keys,
-// while filament_retract_length_nc is defined separately. Keep the generator list
-// unchanged and use this helper for behavior checks that need the full override set.
+// while filament_retract_length_nc and the ironing overrides are defined separately.
+// Keep the generator list unchanged and use this helper for behavior checks that need
+// the full override set. A nil in one of these keys means the override is disabled, so
+// child presets store and load it verbatim instead of treating nil as "inherit".
bool is_filament_extruder_override_key(const std::string &opt_key)
{
return std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end() ||
- opt_key == "filament_retract_length_nc";
+ opt_key == "filament_retract_length_nc" ||
+ opt_key == "filament_ironing_flow" ||
+ opt_key == "filament_ironing_spacing" ||
+ opt_key == "filament_ironing_inset" ||
+ opt_key == "filament_ironing_speed";
}
size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id)
@@ -12272,7 +12278,8 @@ void DynamicPrintConfig::update_diff_values_to_child_config(DynamicPrintConfig&
opt_src->set(opt_target);
}
else
- opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride);
+ // An override's nil means "disabled" and is the child's value; every other nil column inherits.
+ opt_vec_src->set_only_diff(opt_vec_dest, variant_index, stride, is_filament_extruder_override_key(opt));
}
}
}
@@ -13493,12 +13500,30 @@ static std::map 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 +13536,18 @@ static std::map 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& custom_gcode_specific_placeholders()
@@ -13541,11 +13576,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 +13638,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.
diff --git a/src/libslic3r/ShortestPath.cpp b/src/libslic3r/ShortestPath.cpp
index bbea1a6900..31e02f35bc 100644
--- a/src/libslic3r/ShortestPath.cpp
+++ b/src/libslic3r/ShortestPath.cpp
@@ -26,8 +26,10 @@
#include "Print.hpp"
#include "GCode/OrderingStrategies.hpp"
+#include
#include
#include
+#include
#include "MultiMaterialSegmentation.hpp"
namespace Slic3r {
@@ -96,6 +98,46 @@ std::vector> chain_segments_closest_point(std::vector> chain_segments_exact(const std::vector &end_points, const std::vector &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 cost(num_states, std::numeric_limits::max());
+ std::vector 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::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> 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> chain_segments_closest_point(std::vector
std::vector> 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> 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().squaredNorm() < (end_point_func(0, true) - *start_near).template cast().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 end_points(2 * num_segments);
+ std::vector could_reverse(num_segments);
+ for (size_t i = 0; i < num_segments; ++ i) {
+ end_points[2 * i] = end_point_func(i, true).template cast();
+ end_points[2 * i + 1] = end_point_func(i, false).template cast();
+ could_reverse[i] = could_reverse_func(i);
+ }
+ const Vec2d start = start_near != nullptr ? Vec2d(start_near->template cast()) : 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> chain_segments_greedy2(SegmentEndPointFunc
return chain_segments_greedy_constrained_reversals2_(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
+class NearestPoints
+{
+public:
+ static constexpr size_t npos = size_t(-1);
+
+ explicit NearestPoints(const std::vector &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(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 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
+ std::array query(const Vec2d &q, Filter filter) const
+ {
+ std::array, 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 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 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 &m_points;
+ Vec2d m_origin;
+ double m_cell;
+ int m_cols;
+ int m_rows;
+ std::vector m_first;
+ std::vector 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> &chain, const std::vector &end_points,
+ const std::vector &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 order(n), pos(n), fixed_before(n + 1, 0);
+ std::vector 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::npos;
+ const NearestPoints nearest(end_points);
+ std::vector> 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 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 queue;
+ std::vector 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::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> chain_nearest_next(const std::vector &end_points, const std::vector &could_reverse, const Vec2d &start)
+{
+ const NearestPoints<1> nearest(end_points);
+ std::vector taken(could_reverse.size(), 0);
+ std::vector> 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> &chain, const std::vector &end_points, const Vec2d &start)
+{
+ double travel = 0.;
+ Vec2d at = start;
+ for (const std::pair &segment : chain) {
+ travel += (end_points[2 * segment.first + segment.second] - at).norm();
+ at = end_points[2 * segment.first + 1 - segment.second];
+ }
+ return travel;
+}
+
template
-static std::vector> chain_extrusion_entities_impl(const std::vector &entities, const Point *start_near)
+static std::vector> chain_extrusion_entities_impl(const std::vector &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> out = chain_segments_greedy_constrained_reversals(segment_end_point, could_reverse, entities.size(), start_near);
+ if (improve && out.size() > 1) {
+ std::vector end_points;
+ std::vector 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());
+ end_points.emplace_back(segment_end_point(idx, false).template cast());
+ reversible.emplace_back(could_reverse(idx));
+ }
+ Vec2d start = Vec2d::Zero();
+ if (start_near != nullptr)
+ start = start_near->cast();
+ 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> 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 &segment : out) {
const ExtrusionEntity *ee = entities[segment.first];
if (ee->is_loop())
@@ -1086,7 +1500,8 @@ void reorder_extrusion_entities(std::vector &entities, const s
void chain_and_reorder_extrusion_entities(std::vector &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 &entities, const Point *start_near)
@@ -1101,7 +1516,7 @@ void chain_and_reorder_extrusion_entities(std::vector &e
remove_entities_without_endpoints(entities);
std::vector 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 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 &path, const Points &points)
+{
+ const size_t pn = path.size();
+ if (pn < 5)
+ return false;
+ std::vector coords(points.size());
+ for (size_t i = 0; i < points.size(); ++ i)
+ coords[i] = points[i].cast();
+ const NearestPoints<8> grid(coords);
+ std::vector 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 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 chain_points_with_postprocessing(const Points &points, const Point *start_near)
{
- std::vector 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 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().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);
diff --git a/src/libslic3r/ShortestPath.hpp b/src/libslic3r/ShortestPath.hpp
index 53ec00f9e4..34127fdb9a 100644
--- a/src/libslic3r/ShortestPath.hpp
+++ b/src/libslic3r/ShortestPath.hpp
@@ -23,9 +23,12 @@ std::vector chain_expolygons(const ExPolygons &input_exploy);
std::vector> chain_extrusion_entities(std::vector &entities, const Point *start_near = nullptr);
void reorder_extrusion_entities(std::vector &entities, const std::vector> &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 &entities, const Point &start_near);
+// The greedy chain only.
void chain_and_reorder_extrusion_entities(std::vector &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 &entities, const Point &start_near,
std::vector> &reversed_clones);
diff --git a/src/slic3r/GUI/DailyTips.cpp b/src/slic3r/GUI/DailyTips.cpp
index 8bace894e1..dc97117efc 100644
--- a/src/slic3r/GUI/DailyTips.cpp
+++ b/src/slic3r/GUI/DailyTips.cpp
@@ -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];
diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp
index f6adfc0c38..5d467d44c7 100644
--- a/src/slic3r/GUI/GUI_Factories.cpp
+++ b/src/slic3r/GUI/GUI_Factories.cpp
@@ -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();
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp
index db125ae1d5..ae39eade25 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp
@@ -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;
diff --git a/src/slic3r/GUI/HMS.cpp b/src/slic3r/GUI/HMS.cpp
index 2234d1afb4..6bd847f32f 100644
--- a/src/slic3r/GUI/HMS.cpp
+++ b/src/slic3r/GUI/HMS.cpp
@@ -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 package_dev_id_types {"094", "239", "093", "22E"};
+static std::unordered_set 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()
diff --git a/src/slic3r/GUI/Jobs/PrintJob.cpp b/src/slic3r/GUI/Jobs/PrintJob.cpp
index b6b420969f..c903e3b139 100644
--- a/src/slic3r/GUI/Jobs/PrintJob.cpp
+++ b/src/slic3r/GUI/Jobs/PrintJob.cpp
@@ -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())
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index 058eae58ba..3b224725f6 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -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(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(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(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(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 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("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::get_enum_names().at(print_preset.config.opt_enum("support_type"));
- j["sparse_infill_pattern"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("sparse_infill_pattern"));
- j["sparse_infill_density"] = print_preset.config.opt("sparse_infill_density")->value;
-
- j["brim_type"] = ConfigOptionEnum::get_enum_names().at(print_preset.config.opt_enum("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 different_values = full_config.option("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
diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp
index 564addb0c7..a879226b42 100644
--- a/src/slic3r/GUI/Plater.hpp
+++ b/src/slic3r/GUI/Plater.hpp
@@ -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);
diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp
index 3f5bf126a2..26441baf37 100644
--- a/src/slic3r/GUI/SelectMachine.cpp
+++ b/src/slic3r/GUI/SelectMachine.cpp
@@ -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("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) {
diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp
index 7fde77a075..30fd075fc4 100644
--- a/src/slic3r/Utils/BBLPrinterAgent.cpp
+++ b/src/slic3r/Utils/BBLPrinterAgent.cpp
@@ -7,6 +7,8 @@
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/DeviceCore/DevManager.h"
+#include
+#include
#include
#include
#include