Compare commits

...
Author SHA1 Message Date
Ian Chua e9d1bb85b8 fix: shim python create thread to audit threads 2026-10-20 13:49:07 +08:00
Florian Benkö c9d897b4d0 Fix send dialog crash for a single-nozzle plate on a two-extruder printer (#16599)
update_material_item_pos() chose the left or right nozzle panel from the printer alone and then read m_filaments_map, which reset_and_sync_ams_list() fills only for a multi-nozzle plate. Clicking Print plate with a plate sliced for one nozzle (A1) and a two-extruder printer selected (X2D) read the empty map and crashed. For a single-nozzle plate the dialog now uses the single panel.
2026-10-19 20:52:15 -05:00
Mihai Mocanu 6bff3c3cd6 Fix build_linux.sh distro detection when os-release uses single quotes (#16566)
Gentoo writes `ID='gentoo'` to /etc/os-release. The script only stripped double quotes, so `-u` looked for `scripts/linux.d/'gentoo'` and exited. Single quotes are now stripped too, for both `ID` and `ID_LIKE`.

Fixes #16567
2026-10-19 20:49:37 -05:00
S H 6e1d15f8a2 Avoid signed-shift undefined behavior in EdgeGrid distance propagation (#16626)
Danielsson propagation in EdgeGrid::Grid::calculate_sdf() doubles a negative neighbor offset (-1 or -ncols) with a left shift, which is undefined behavior in C++17. Multiply by two instead.

Adds test_edge_grid.cpp, which checks signed distances inside and outside a rectangle beyond the initial edge band. Built with -fsanitize=shift, it aborts on the old code and passes with the fix.
2026-10-19 20:44:24 -05:00
Colin Trachte 4f63cd4051 Add Reload from disk to multi-selection menu (#16589)
Show Reload from disk in the right-click menu when several objects or parts are selected, as it already is for a single object or part. reload_from_disk() already handles selections across several objects, the same way Reload All uses it.

Fixes #10349
2026-10-19 20:43:19 -05:00
Downtime c7489d431d fix(HMS): load packaged data for additional printer families (#16267)
Add 20P, 26A and 31B to package_dev_id_types so Orca loads the HMS files it already ships for them. Before this, HMS errors on these printers fell through to the cloud lookup and could show as "Unknown error."
2026-10-19 20:40:17 -05:00
HanifKoh 506dd497db Translate H2C Nozzle Rack Filament IDs at the Printer Boundary (#16612)
The Bambu payload rewrite only matched tray_info_idx, filament_id and filamentId. The H2C nozzle rack puts filament ids in cate (the auto nozzle mapping request) and fila_id (the rack status), so the printer got Orca's OF ids for the filaments and its own ids back for the nozzles in the same request, and the device tab could not name a rack nozzle's filament. Add both keys to one shared list used by the rewrite and its pre-check.
2026-10-19 19:42:05 -05:00
HanifKoh 56da2f43b0 Remove the Unused Slice Preset Record That Raced the Slicing Thread (#16649)
`Plater::record_slice_preset` built a json from the current presets and then discarded it. It also read `full_print_config()` on the UI thread while the slicing thread's filament-map write-back reassigned it. On multi-filament projects this crashed Orca right after a slice started.

Remove the function and its two callers in `Plater::reslice()` and `PrintJob::process`.
2026-10-19 19:40:09 -05:00
yw4z 00838d59c1 Remove unused NotoSansKR fonts and fix the emboss fallback font (#16665)
Remove `NotoSansKR-Regular.ttf` and `NotoSansKR-Bold.ttf` from `resources/fonts` (12.4 MB). No code loads them. Korean UI text uses NanumGothic. The files came back by accident with the BambuStudio 1.8 merge (#2844), and macOS and the Flatpak still registered them as app fonts.

Emboss text that uses "Noto Sans KR" on macOS or Flatpak now shows the "Can't load exactly same font" warning when edited.

The emboss tool's fallback for a system with no usable TTF font pointed at `NotoSans-Regular.ttf`, which Orca does not ship. It now uses `HarmonyOS_Sans_SC_Regular.ttf`.
2026-10-19 19:36:54 -05:00
Downtime 8f8bfd6a28 fix: align custom gcode placeholder metadata (#16272)
Declare the runtime placeholders that filament, layer-change, timelapse, wrapping-detection, machine-end and change-filament G-code receive, so the Edit G-code dialog can list them and a build with ORCA_CHECK_GCODE_PLACEHOLDERS no longer rejects them. Adds the missing toolchange_z definition and wraps the new labels and tooltips in L() so they get translated.
2026-10-19 19:20:51 -05:00
Robert J AudasandRF47 90a5a0b964 Fix JPEG texture imports and filament-matching crash (#16607)
Co-authored-by: RF47 <162915171+RF47@users.noreply.github.com>
2026-10-19 20:30:03 -03:00
Kris Austin f64beb4379 Fix random crash while slicing with the Russian UI (daily tips) (#16664)
Fix crash when a daily tip has no body text

has_cjk() looped while i < text.size() - 2. For a string shorter than
two bytes the size_t subtraction wraps, so on an empty string the loop
read past the buffer and up the stack until it hit an unmapped page.

The Russian translation of the "Reverse on even" tip had lost the \n
after its title, so render_text() passed an empty body to has_cjk()
every time that tip was shown during slicing. Restore the line break
and bound the loop so a translation like this can't crash the app.
2026-10-19 16:25:07 -05:00
Kris Austin a2b1a2f3e7 Keep an offline Moonraker printer from freezing startup (#16668)
Moonraker discovery asked the printer for its name before announcing
it, on the GUI thread, with a 5 s connect timeout. When the selected
printer was off, the window ignored input for 5 s after every launch
and the Home page stayed white.

The request never produced a name. It read machine_name or hostname
from /server/info, and Moonraker sends neither there. Discovery now
announces the preset model name without asking the printer.
2026-10-19 16:21:59 -05:00
yw4z 0585836550 Fetch cloud plugin metadata faster (#16645)
When logged in to Orca Cloud, the plugin fetch at startup no longer refreshes the token a second time while start() is already refreshing it. It now waits for that refresh and checks the token again. The subscribed and owned plugin requests also run in parallel, so the fetch takes about half as long.
2026-10-19 15:56:52 -05:00
5a36120c58 fix(gcode): use Klipper motion planning for time estimates (#15545)
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Co-authored-by: raistlin7447 <kris.austin@gmail.com>
2026-10-19 16:15:59 -03:00
Ian BassiandTobias Gloth 69193d5681 Optimize travel: exact/refined path chaining, object tours, and joined infill lines (#16492)
* Shorten the travel between extrusion paths

chain_and_reorder_extrusion_entities() builds the print order of the infill,
ironing and support paths of a region from the nozzle position. It took the
greedy chain, which leaves its longest joins for last; it now shortens it:

- Up to eight paths, chain_segments_exact() finds the shortest chain over
  every order and allowed reversal, by dynamic programming over the subsets
  of paths, with a fixed start and a free end.
- Longer chains are refined by local search: 2-opt, or-opt of up to three
  paths, and moving a run of any length next to the nozzle to the front, each
  tried only toward the 32 end points nearest a link, which come from a
  uniform grid. A move is applied only if it shortens the chain, so the result
  is never longer than the greedy one.
- A nearest-next chain is built too, and refined and used instead when it is
  already shorter than the refined greedy chain, which happens when the greedy
  chain sweeps away from the nozzle and has to come back with a long jump.

The shared greedy chaining also chains two to four segments exactly, which
covers the islands of a layer and the loops of a perimeter collection. Paths
that cannot reverse, such as Grid's, are only reordered, and collections that
keep their order are left alone. The overload without a start point keeps the
greedy chain.

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>

* Order objects and islands along shorter tours

Objects under 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. Up to ten points, tsp_exact_cycle() finds the
shortest tour by dynamic programming over subsets, starting at the point
nearest the start point when one is given. Larger sets add or-opt to the
existing 2-opt and crossing removal: runs of up to three points move next to
one of the eight points nearest an end of the run, reversed or not, and
crossing removal runs last so no crossing is left. Snake ordering is
unchanged; Best of strategies gets the shorter Default tour as a candidate.

* Join infill lines into fewer paths

Every separate infill path costs a travel to it, so the fill generators now
leave fewer of them:

- connect_infill() pairs the line ends along each wall in the order that
  joins the most lines: besides shortest arc first, it replays arcs of about
  the same length taken along each contour, forwards and backwards, including
  the last pass that joins neighboring open ends, and keeps the order that
  joins the most. Regular patterns no longer leave loose ends where the
  pairs along a wall fall out of step, and multiline copies print out and back.
- Ends whose anchors would meet are joined along the wall instead, up to two
  anchors and a line width apart.
- An unpaired end anchors toward the nearest open end of another path, where
  the next path usually starts, instead of the side with more free wall. With
  multiline infill, an anchor toward a copy of the same line stops short of
  the copy instead of printing over it.
- Last, sparse infill lines whose ends are at most two line spacings apart are
  joined with a straight piece, shortest first, without closing a loop or
  crossing a line; with anchors off this is the only joining. Copies of
  neighboring multiline groups are joined at the same side, and lines that
  cannot reverse only join end to start.

* Describe the travel optimization design

How the extrusion chains, the object and island tours and the infill line joining shorten the travel, and why each part is built the way it is.

---------

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>
2026-10-19 15:53:32 -03:00
Kris Austin cdb9bb57ae fix: Mesh Boolean crash on Windows builds made with clang-cl (#16651)
Build without precompiled headers when the compiler is clang-cl, in both the slicer and the wxWidgets dependency, and add a CI check that fails if broken exception throw info comes back.

With a precompiled header, clang-cl can leave the copy constructor out of the throw info it emits for an exception type (llvm/llvm-project#53486, open since 2022 and still present in clang 22.1.3). The linker keeps one throw info per type for the whole binary. When it keeps one from a precompiled header file, a copy of that exception is bitwise, and the copy and the original free the same data.

Mesh Boolean hits this on every operation. mcut destroys a std::promise that was never set, which stores a broken_promise future_error in an exception_ptr. Freeing it corrupts the heap, and Windows ends the process with 0xC0000374. The 2.5.0-alpha release has broken throw info for 64 exception classes, including std::runtime_error, std::bad_alloc and boost exceptions.

scripts/check_exception_throw_info.ps1 runs after the Windows clang build and its uploads, before any deploy. It fails when an exception class has throw info both with and without a copy constructor, when OrcaSlicer.pdb is missing, or when it finds no exception classes at all. scripts/test_check_exception_throw_info.ps1 tests it on canned dumps.

build_win.bat now treats -l as --no-pch. The Windows ccache key drops the cl.exe version, which only mattered for precompiled headers.

The bug came in with #15428, which moved Windows CI to clang-cl. 2.4.2 was built with MSVC.

Fixes #16581
2026-10-19 12:13:28 -05:00
Sergei f0c59208e7 Apply post-processing scripts to G-code packed into .gcode.3mf (non-BBL) (#16464)
Post-processing scripts now run on a copy of each sliced plate's G-code before it is packed into a .gcode.3mf. This covers Export plate sliced file, sending a .3mf to a print host, and the print dialog for non-BBL printers. BBL printers are skipped, because their G-code is post-processed in place after slicing. Backups are skipped too.

Export plate sliced file no longer reports success when export_3mf() fails.
2026-10-19 10:47:03 -05:00
Ian Bassi 9f6a344162 Make the adaptive infill settings pattern-neutral (#16654) 2026-10-19 12:05:55 -03:00
86 changed files with 4036 additions and 552 deletions

No files matched your search

+8
View File
@@ -19,6 +19,8 @@ on:
- 'build_linux.sh' - 'build_linux.sh'
- 'build_win.bat' - 'build_win.bat'
- 'scripts/test_build_win.ps1' - 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh' - 'build_release_macos.sh'
- 'scripts/build_preset_cache.*' - 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**' - 'scripts/flatpak/**'
@@ -41,6 +43,8 @@ on:
- 'build_linux.sh' - 'build_linux.sh'
- 'build_win.bat' - 'build_win.bat'
- 'scripts/test_build_win.ps1' - 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh' - 'build_release_macos.sh'
- 'scripts/build_preset_cache.*' - 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**' - 'scripts/flatpak/**'
@@ -84,6 +88,10 @@ jobs:
shell: powershell shell: powershell
run: .\scripts\test_build_win.ps1 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: build_linux:
strategy: strategy:
fail-fast: false fail-fast: false
+7 -9
View File
@@ -65,15 +65,6 @@ jobs:
shell: bash shell: bash
run: | run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}" 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_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$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 }} name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe 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 - name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly uses: ./.github/actions/deploy-nightly
+8
View File
@@ -321,6 +321,14 @@ if (IS_CROSS_COMPILE)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE) set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
endif () 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 # Print out the SLIC3R_* cache options
get_cmake_property(_cache_vars CACHE_VARIABLES) get_cmake_property(_cache_vars CACHE_VARIABLES)
list (SORT _cache_vars) list (SORT _cache_vars)
+2 -2
View File
@@ -473,8 +473,8 @@ fi
# cmake 4.x compatibility workaround # cmake 4.x compatibility workaround
export CMAKE_POLICY_VERSION_MINIMUM=3.5 export CMAKE_POLICY_VERSION_MINIMUM=3.5
DISTRIBUTION=$(awk -F= '/^ID=/ {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 '"') DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d "'\"")
# Check for direct distribution match to Ubuntu/Debian # Check for direct distribution match to Ubuntu/Debian
if [ "${DISTRIBUTION}" == "ubuntu" ] || [ "${DISTRIBUTION}" == "linuxmint" ] ; then if [ "${DISTRIBUTION}" == "ubuntu" ] || [ "${DISTRIBUTION}" == "linuxmint" ] ; then
DISTRIBUTION="debian" DISTRIBUTION="debian"
+7 -4
View File
@@ -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 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 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 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_section "Toolchain"
call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler" 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 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%!" set "generator=!vs_gen_%vs_version%!"
if "%use_ninja%" == "ON" ( if "%use_ninja%" == "ON" (
set "generator=Ninja Multi-Config" 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" for %%p in ("!build_dir!") do set "build_full=%%~fp"
echo Configuration: %build_type%, %arch% echo Configuration: %build_type%, %arch%
if not "%clang_exe%" == "" echo Compiler: %clang_exe% 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% if not "%cache_args%" == "" echo Compiler cache: %cache_exe%
set "SIG_FLAG=" set "SIG_FLAG="
@@ -716,8 +719,8 @@ if "%build_slicer%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON" set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON"
) )
REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because REM Under cl, a later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins,
REM CMake takes the last definition on the command line. REM because CMake takes the last definition on the command line.
if "%no_pch%" == "ON" ( if "%no_pch%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF" set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF"
) )
+5 -2
View File
@@ -26,6 +26,7 @@ orcaslicer_add_cmake_project(OpenCV
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277 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 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 CMAKE_ARGS
-DBUILD_SHARED_LIBS=0 -DBUILD_SHARED_LIBS=0
-DBUILD_PERE_TESTS=OFF -DBUILD_PERE_TESTS=OFF
@@ -34,7 +35,8 @@ orcaslicer_add_cmake_project(OpenCV
-DBUILD_EXAMPLES=OFF -DBUILD_EXAMPLES=OFF
-DBUILD_JASPER=OFF -DBUILD_JASPER=OFF
-DBUILD_JAVA=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_APPS_LIST=version
-DBUILD_opencv_apps=OFF -DBUILD_opencv_apps=OFF
-DBUILD_opencv_java=OFF -DBUILD_opencv_java=OFF
@@ -67,7 +69,8 @@ orcaslicer_add_cmake_project(OpenCV
-DWITH_OPENJPEG=OFF -DWITH_OPENJPEG=OFF
-DWITH_QUIRC=OFF -DWITH_QUIRC=OFF
-DWITH_VTK=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_WEBP=OFF
-DWITH_TIFF=OFF -DWITH_TIFF=OFF
-DBUILD_TIFF=OFF -DBUILD_TIFF=OFF
+9 -1
View File
@@ -21,6 +21,14 @@ else ()
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF") set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
endif () 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 "") set(_wx_patch_command "")
if (APPLE) if (APPLE)
set(_wx_patch_command set(_wx_patch_command
@@ -39,7 +47,7 @@ orcaslicer_add_cmake_project(
PATCH_COMMAND ${_wx_patch_command} PATCH_COMMAND ${_wx_patch_command}
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG} DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
CMAKE_ARGS CMAKE_ARGS
-DwxBUILD_PRECOMP=ON -DwxBUILD_PRECOMP=${_wx_precomp}
${_wx_toolkit} ${_wx_toolkit}
"-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}" "-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}"
-DwxBUILD_DEBUG_LEVEL=0 -DwxBUILD_DEBUG_LEVEL=0
@@ -1,8 +1,8 @@
# Adaptive TPMS infill — High Level Design # Adaptive infill — High Level Design
## Purpose and scope ## Purpose and scope
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK `adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top `stepped_shells` follow the distance to the nearest surface, including the top
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x` the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it. change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `tpms_interior_density` `sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density the density at the center, and `adaptive_infill_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it. Z-buckling optimization does not apply to it.
@@ -22,9 +22,9 @@ split into shells or blended between densities.
## Radial field ## Radial field
`TpmsRadialField` gives every point of an object the center of its lobe and a `AdaptiveInfillField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_tpms_radial_fields()` builds it in center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring `bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the every mode a region uses, and is shared by the regions using that mode: the
@@ -202,7 +202,7 @@ built for them.
## Constraints ## Constraints
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset - With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches. to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two - `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large. densities, as the voids at the center are that large.
+5 -2
View File
@@ -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 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: while no agent is live. Whole documents are Bambu's business alone:
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under `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 `tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed 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. file degrades to identity with a log line rather than failing.
| Boundary | Where it translates | | Boundary | Where it translates |
+198
View File
@@ -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.
+3 -1
View File
@@ -36839,7 +36839,9 @@ msgstr ""
msgid "" msgid ""
"Reverse on even\n" "Reverse on even\n"
"Did you know that <b>Reverse on even</b> feature can significantly improve the surface quality of your overhangs? However, it can cause wall inconsistencies so use carefully!" "Did you know that <b>Reverse on even</b> feature can significantly improve the surface quality of your overhangs? However, it can cause wall inconsistencies so use carefully!"
msgstr "Реверс на чётных слояхСовет: настройка <b>Реверс на чётных слоях</b> может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью." msgstr ""
"Реверс на чётных слоях\n"
"Совет: настройка <b>Реверс на чётных слоях</b> может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью."
#: resources/data/hints.ini: [hint:Cut Tool] #: resources/data/hints.ini: [hint:Cut Tool]
msgid "" msgid ""
Binary file not shown.
Binary file not shown.
+126
View File
@@ -0,0 +1,126 @@
<#
.SYNOPSIS
Fails when a Windows build has an exception class with broken throw info.
.DESCRIPTION
Clang on the MSVC ABI can leave the copy constructor out of the CatchableType it emits for
an exception class that comes through a precompiled header
(https://github.com/llvm/llvm-project/issues/53486). The linker keeps one throw info per
class, so when it keeps such a record, a copy of that exception (std::exception_ptr, a catch
by value) shares its data with the original and the data is freed twice.
A class is either trivially copyable or not, so a binary should never have both a CatchableType
without a copy constructor (_CT??_R0?AV<class>@@@8<size>) and one with it
(_CT??_R0?AV<class>@@@8??0<class>...). The script lists each PDB's public symbols with the
llvm-pdbutil next to the build's compiler and reports every class that has both.
scripts\test_check_exception_throw_info.ps1 tests it.
.PARAMETER BuildDir
The slicer build directory. Its CMakeCache.txt names the compiler, and its src\<Config>
holds the PDBs to check, which must include OrcaSlicer.pdb.
.PARAMETER Config
The configuration whose PDBs to check (default: Release).
.PARAMETER Pdb
PDBs to check instead of the ones in <BuildDir>\src\<Config>, as a list or comma-separated.
.PARAMETER Dump
Saved `llvm-pdbutil dump --publics` output to check instead of PDBs. Needs no build.
.EXAMPLE
scripts\check_exception_throw_info.ps1 -BuildDir build
#>
param(
[string] $BuildDir,
[string] $Config = 'Release',
[string[]] $Pdb,
[string[]] $Dump
)
$ErrorActionPreference = 'Stop'
# powershell -File passes "a.pdb,b.pdb" as one string. A path that exists as given stays whole.
function Split-PathList([string[]] $values) {
@($values | Where-Object { $_ } | ForEach-Object {
if ((Test-Path -LiteralPath $_) -or $_ -notmatch ',') { $_ } else { $_ -split ',' | Where-Object { $_ } }
})
}
$Dump = Split-PathList $Dump
if (-not $Dump) {
if (-not $BuildDir) {
throw 'Name the slicer build directory with -BuildDir.'
}
$cache = Join-Path $BuildDir 'CMakeCache.txt'
if (-not (Test-Path $cache)) {
throw "No CMakeCache.txt in $BuildDir. Name the slicer build directory."
}
$entry = Select-String -Path $cache -Pattern '^CMAKE_CXX_COMPILER:\w+=(.+)$' | Select-Object -First 1
if (-not $entry) {
throw "No CMAKE_CXX_COMPILER in $cache."
}
$compiler = $entry.Matches[0].Groups[1].Value
# A tree configured with a bare compiler name found it on PATH.
if (-not (Split-Path $compiler)) {
$found = Get-Command $compiler -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $found) {
throw "$compiler, the compiler in $cache, is not on PATH."
}
$compiler = $found.Source
}
$pdbutil = Join-Path (Split-Path $compiler) 'llvm-pdbutil.exe'
if (-not (Test-Path $pdbutil)) {
throw "No llvm-pdbutil.exe next to $compiler. This check needs a build made with Clang."
}
$Pdb = Split-PathList $Pdb
if (-not $Pdb) {
# OrcaSlicer.dll holds the slicer itself, so a check without it proves nothing.
$pdbDir = Join-Path $BuildDir "src\$Config"
if (-not (Test-Path (Join-Path $pdbDir 'OrcaSlicer.pdb'))) {
throw "No OrcaSlicer.pdb in $pdbDir."
}
$Pdb = @(Get-ChildItem $pdbDir -Filter *.pdb | ForEach-Object FullName)
}
}
$catchable = [regex]'`_CT\?\?_R0\?A[UV](.+?)@@@8([^`]*)`'
$tmp = [IO.Path]::GetTempFileName()
$failed = $false
$classes = 0
try {
foreach ($file in $(if ($Dump) { $Dump } else { $Pdb })) {
$text = $file
if (-not $Dump) {
$run = Start-Process -FilePath $pdbutil -ArgumentList 'dump', '--publics', "`"$file`"" -NoNewWindow -Wait -PassThru -RedirectStandardOutput $tmp
if ($run.ExitCode -ne 0) {
throw "llvm-pdbutil failed on $file."
}
$text = $tmp
}
$kinds = @{}
foreach ($match in $catchable.Matches([IO.File]::ReadAllText((Resolve-Path -LiteralPath $text).Path))) {
$class = $match.Groups[1].Value
if (-not $kinds.ContainsKey($class)) { $kinds[$class] = @{} }
$kinds[$class][$(if ($match.Groups[2].Value.StartsWith('??0')) { 'copy' } else { 'bitwise' })] = $true
}
$broken = @($kinds.Keys | Where-Object { $kinds[$_].Count -eq 2 } | Sort-Object)
"$(Split-Path -Leaf $file): $($kinds.Count) exception classes, $($broken.Count) with broken throw info"
foreach ($class in $broken) { " $class" }
if ($broken.Count -gt 0) { $failed = $true }
$classes += $kinds.Count
}
} finally {
Remove-Item $tmp -ErrorAction SilentlyContinue
}
# The slicer's PDBs always contain exception throw info, so finding none means the dump format or the PDBs changed.
if ($classes -eq 0) {
'No exception classes found. Check that llvm-pdbutil still prints public symbols in backticks.'
$failed = $true
}
exit [int]$failed
+4
View File
@@ -301,6 +301,10 @@ $cases = @(
Contains = @('Precompiled header: off') } Contains = @('Precompiled header: off') }
@{ Name = 'the precompiled header is on unless asked'; Args = @('-s') @{ Name = 'the precompiled header is on unless asked'; Args = @('-s')
NotContains = @('SLIC3R_PCH') } 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') @{ Name = '--no-pch works without a cache'; Args = @('-s', '--no-pch')
NotContains = @('COMPILER_LAUNCHER') } NotContains = @('COMPILER_LAUNCHER') }
@{ Name = 'the slicer build runs gettext'; Args = @('-s') @{ Name = 'the slicer build runs gettext'; Args = @('-s')
@@ -0,0 +1,93 @@
<#
.SYNOPSIS
Tests scripts/check_exception_throw_info.ps1 on canned llvm-pdbutil dumps.
.DESCRIPTION
Each case runs the check in its own powershell -File process, as CI does, and asserts on the
exit code and output. Dumps are written to a temporary folder, so no build is needed.
.EXAMPLE
powershell -File scripts/test_check_exception_throw_info.ps1
#>
$ErrorActionPreference = 'Stop'
$check = Join-Path $PSScriptRoot 'check_exception_throw_info.ps1'
$work = Join-Path ([IO.Path]::GetTempPath()) 'check_exception_throw_info_tests'
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $work | Out-Null
function Write-Dump([string] $path, [string[]] $symbols) {
New-Item -ItemType Directory -Force -Path (Split-Path $path) | Out-Null
$i = 0
$lines = @(' Public Symbols', '============================================================')
foreach ($symbol in $symbols) {
$i++
$lines += " $i | S_PUB32 [size = 48] ``$symbol``", ' flags = none, addr = 0007:0000'
}
Set-Content -Path $path -Encoding ascii -Value $lines
}
$bitwise = '_CT??_R0?AVfuture_error@std@@@840'
$copy = '_CT??_R0?AVfuture_error@std@@@8??0future_error@std@@QEAA@AEBV01@@Z40'
$trivial = '_CT??_R0?AUtag@@@81'
Write-Dump "$work\broken.txt" @($bitwise, $copy, $trivial)
Write-Dump "$work\clean.txt" @($copy, $trivial)
Write-Dump "$work\empty.txt" @()
Write-Dump "$work\a dir, with a comma\clean.txt" @($copy)
# A build directory whose compiler has an llvm-pdbutil beside it but no OrcaSlicer.pdb.
# Nothing runs the stand-in; the check stops before it gets there.
$build = "$work\build"
New-Item -ItemType Directory -Force -Path "$build\bin", "$build\src\Release" | Out-Null
Copy-Item "$env:SystemRoot\System32\where.exe" "$build\bin\llvm-pdbutil.exe"
Set-Content -Path "$build\CMakeCache.txt" -Encoding ascii -Value "CMAKE_CXX_COMPILER:FILEPATH=$build\bin\clang-cl.exe"
Set-Content -Path "$build\src\Release\orca-slicer.pdb" -Value ''
$bare = "$work\bare"
New-Item -ItemType Directory -Force -Path $bare | Out-Null
Set-Content -Path "$bare\CMakeCache.txt" -Encoding ascii -Value 'CMAKE_CXX_COMPILER:STRING=no-such-clang-cl.exe'
$cases = @(
@{ Name = 'a class with and without a copy constructor fails'; Args = @('-Dump', "$work\broken.txt")
Exit = 1; Contains = @('1 with broken throw info', 'future_error@std') }
@{ Name = 'copy constructors and trivially copyable classes pass'; Args = @('-Dump', "$work\clean.txt")
Exit = 0; Contains = @('2 exception classes, 0 with broken throw info') }
@{ Name = 'no exception classes at all fails'; Args = @('-Dump', "$work\empty.txt")
Exit = 1; Contains = @('No exception classes found') }
@{ Name = 'a comma-separated list checks each dump'; Args = @('-Dump', "$work\broken.txt,$work\clean.txt")
Exit = 1; Contains = @('broken.txt: 2 exception classes', 'clean.txt: 2 exception classes') }
@{ Name = 'a path with a comma stays whole'; Args = @('-Dump', "$work\a dir, with a comma\clean.txt")
Exit = 0; Contains = @('clean.txt: 1 exception classes') }
@{ Name = 'a build without OrcaSlicer.pdb fails'; Args = @('-BuildDir', $build)
Exit = 1; Contains = @('No OrcaSlicer.pdb in') }
@{ Name = 'a compiler that is not on PATH is named'; Args = @('-BuildDir', $bare)
Exit = 1; Contains = @('no-such-clang-cl.exe, the compiler in') }
@{ Name = 'a missing build directory is named'; Args = @('-BuildDir', "$work\nowhere")
Exit = 1; Contains = @('No CMakeCache.txt in') }
@{ Name = 'no build directory and no dump fails'; Args = @()
Exit = 1; Contains = @('Name the slicer build directory') }
)
$failed = 0
foreach ($case in $cases) {
$ErrorActionPreference = 'Continue'
$output = (& powershell -NoProfile -File $check @($case.Args) 2>&1 | Out-String)
$code = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
$problems = @()
if ($code -ne $case.Exit) { $problems += "exit $code, expected $($case.Exit)" }
foreach ($text in $case.Contains) {
if (-not $output.Contains($text)) { $problems += "no '$text' in the output" }
}
if ($problems) {
$failed++
" FAIL $($case.Name)"
$problems | ForEach-Object { " $_" }
$output.TrimEnd() -split "`r?`n" | ForEach-Object { " | $_" }
} else {
" ok $($case.Name)"
}
}
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
"$($cases.Count - $failed) passed, $failed failed"
exit [int]($failed -gt 0)
+5 -2
View File
@@ -182,8 +182,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp Fill/FillRectilinear.hpp
Fill/FillTpmsAdaptive.cpp Fill/AdaptiveInfillField.cpp
Fill/FillTpmsAdaptive.hpp Fill/AdaptiveInfillField.hpp
Fill/FillTpmsD.cpp Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp Fill/FillTpmsFK.cpp
@@ -666,6 +666,9 @@ endif ()
encoding_check(libslic3r) encoding_check(libslic3r)
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0) 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) if (USE_SLIC3R_CONSOLE_LOG)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>) target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
endif() endif()
+1 -1
View File
@@ -616,7 +616,7 @@ struct PropagateDanielssonSingleStep {
if ((signs[addr] & 2) == 0) { if ((signs[addr] & 2) == 0) {
float *v = &L[addr << 1]; float *v = &L[addr << 1];
float l = v[0] * v[0] + v[1] * v[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] = { float v2[2] = {
v2s[0] + INCX * resolution, v2s[0] + INCX * resolution,
v2s[1] + INCY * resolution v2s[1] + INCY * resolution
@@ -1,4 +1,4 @@
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
#include <algorithm> #include <algorithm>
#include <array> #include <array>
@@ -146,11 +146,11 @@ void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, i
} }
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point. // Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth) double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
{ {
switch (gradient) { switch (gradient) {
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth; case AdaptiveInfillGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth); case AdaptiveInfillGradient::Exponential: return std::pow(ratio, depth);
default: return 1. + (ratio - 1.) * depth; default: return 1. + (ratio - 1.) * depth;
} }
} }
@@ -159,7 +159,7 @@ double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
// continuous level of the target of a depth. // continuous level of the target of a depth.
struct DensityLevels struct DensityLevels
{ {
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient) DensityLevels(double ratio, double step, AdaptiveInfillGradient gradient)
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON))) : ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
{} {}
@@ -170,7 +170,7 @@ struct DensityLevels
} }
double ratio; double ratio;
TpmsAdaptiveGradient gradient; AdaptiveInfillGradient gradient;
int count; int count;
}; };
@@ -212,12 +212,12 @@ private:
} // namespace } // namespace
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode, AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
const std::function<void()> &throw_if_canceled) const std::function<void()> &throw_if_canceled)
: m_mode(mode) : m_mode(mode)
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1) , m_axis(mode == AdaptiveInfillMode::NormalX ? 0 : mode == AdaptiveInfillMode::NormalY ? 1 : mode == AdaptiveInfillMode::NormalZ ? 2 : -1)
{ {
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled); assert(!slices.empty() && mode != AdaptiveInfillMode::Disabled);
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z); const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min; const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
@@ -269,7 +269,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the // Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center // centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
// is the node nearest to the middle of the tied nodes. // is the node nearest to the middle of the tied nodes.
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend; const bool lobes = m_mode != AdaptiveInfillMode::SteppedShells && m_mode != AdaptiveInfillMode::SmoothBlend;
m_body.assign(depth.size(), -1); m_body.assign(depth.size(), -1);
std::vector<size_t> body_nodes; std::vector<size_t> body_nodes;
for (size_t seed = 0; seed < depth.size(); ++seed) { for (size_t seed = 0; seed < depth.size(); ++seed) {
@@ -341,7 +341,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
} }
throw_if_canceled(); throw_if_canceled();
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) { if (m_mode == AdaptiveInfillMode::SteppedShells || m_mode == AdaptiveInfillMode::SmoothBlend || m_mode == AdaptiveInfillMode::DistanceWarp) {
m_depth.assign(depth.size(), 0.f); m_depth.assign(depth.size(), 0.f);
for (size_t i = 0; i < depth.size(); ++i) for (size_t i = 0; i < depth.size(); ++i)
if (m_body[i] >= 0) if (m_body[i] >= 0)
@@ -421,7 +421,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
lobe.reach.resize(log_reach.size()); lobe.reach.resize(log_reach.size());
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); }); std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
if (m_mode == TpmsAdaptiveMode::DistanceWarp) { if (m_mode == AdaptiveInfillMode::DistanceWarp) {
std::vector<double> mean(log_reach.size() * (Samples + 1)); std::vector<double> mean(log_reach.size() * (Samples + 1));
for (size_t d = 0; d < log_reach.size(); ++d) { for (size_t d = 0; d < log_reach.size(); ++d) {
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth); const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
@@ -488,7 +488,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
} }
} }
double TpmsRadialField::depth(const Vec3d &pt) const double AdaptiveInfillField::depth(const Vec3d &pt) const
{ {
assert(!m_depth.empty()); assert(!m_depth.empty());
const Vec3d f = (pt - m_origin) / m_cell; const Vec3d f = (pt - m_origin) / m_cell;
@@ -508,7 +508,7 @@ double TpmsRadialField::depth(const Vec3d &pt) const
return value; return value;
} }
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
{ {
Vec3d d = pt - center; Vec3d d = pt - center;
if (m_axis >= 0) if (m_axis >= 0)
@@ -516,7 +516,7 @@ Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const
return d; return d;
} }
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
{ {
const Vec3d d = this->offset(pt, lobe.center); const Vec3d d = this->offset(pt, lobe.center);
const double r = d.norm(); const double r = d.norm();
@@ -562,7 +562,7 @@ double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
return warp(mean); return warp(mean);
} }
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
{ {
assert(!this->empty()); assert(!this->empty());
Vec3i32 idx; Vec3i32 idx;
@@ -588,7 +588,7 @@ size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
return count; return count;
} }
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const size_t AdaptiveInfillField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
{ {
const Body &body = m_bodies[m_body[node]]; const Body &body = m_bodies[m_body[node]];
if (body.lobes == 1) { if (body.lobes == 1) {
@@ -641,7 +641,7 @@ struct AdaptiveTpmsField
const long gsize = std::lround(gsizef / rsizef); const long gsize = std::lround(gsizef / rsizef);
const AdaptiveTpms &tpms; const AdaptiveTpms &tpms;
const TpmsRadialField &radial_field; const AdaptiveInfillField &radial_field;
RadialScale scale; RadialScale scale;
DensityLevels levels; DensityLevels levels;
Point size; Point size;
@@ -650,7 +650,7 @@ struct AdaptiveTpmsField
double cos_angle; double cos_angle;
double sin_angle; double sin_angle;
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle) AdaptiveTpmsField(const AdaptiveTpms &tpms, const AdaptiveInfillField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2) : tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient) , levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
, size(bbox.size()), offs(bbox.min), z(z) , size(bbox.size()), offs(bbox.min), z(z)
@@ -666,7 +666,7 @@ struct AdaptiveTpmsField
const double x = unscaled(pt.x()); const double x = unscaled(pt.x());
const double y = unscaled(pt.y()); const double y = unscaled(pt.y());
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z); const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) { if (radial_field.mode() == AdaptiveInfillMode::SmoothBlend) {
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep. // The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
auto lattice = [this, x, y](int level) { auto lattice = [this, x, y](int level) {
const double frequency = tpms.surface_frequency * levels.scale(level); const double frequency = tpms.surface_frequency * levels.scale(level);
@@ -681,7 +681,7 @@ struct AdaptiveTpmsField
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.)); return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
} }
const int axis = radial_field.axis(); const int axis = radial_field.axis();
TpmsRadialField::Radials radials; AdaptiveInfillField::Radials radials;
const size_t count = radial_field.radial(obj, radials); const size_t count = radial_field.radial(obj, radials);
float value = 0.f; float value = 0.f;
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {
@@ -708,20 +708,20 @@ template<> struct _RasterTraits<AdaptiveTpmsField>
}; };
// Continuous density level of the depth over a layer, in the object frame. // Continuous density level of the depth over a layer, in the object frame.
struct TpmsLevelField struct AdaptiveLevelField
{ {
static constexpr float gsizef = 0.5f; static constexpr float gsizef = 0.5f;
static constexpr float rsizef = 0.05f; static constexpr float rsizef = 0.05f;
const coord_t rsize = scaled(rsizef); const coord_t rsize = scaled(rsizef);
const long gsize = std::lround(gsizef / rsizef); const long gsize = std::lround(gsizef / rsizef);
const TpmsRadialField &field; const AdaptiveInfillField &field;
const DensityLevels &levels; const DensityLevels &levels;
Point size; Point size;
Point offs; Point offs;
double z; double z;
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z) AdaptiveLevelField(const AdaptiveInfillField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z) : field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
{} {}
@@ -736,19 +736,19 @@ struct TpmsLevelField
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); } inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
}; };
template<> struct _RasterTraits<TpmsLevelField> template<> struct _RasterTraits<AdaptiveLevelField>
{ {
using ValueType = float; using ValueType = float;
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); } static float get(const AdaptiveLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); } static size_t rows(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); } static size_t cols(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.x()); }
}; };
} // namespace marchsq } // namespace marchsq
namespace Slic3r { namespace Slic3r {
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox, Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle) coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
{ {
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region. // A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
@@ -775,21 +775,21 @@ Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &fi
return polylines; return polylines;
} }
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient) float surface_density, float interior_density, AdaptiveInfillGradient gradient)
{ {
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient); const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
if (levels.count == 0) if (levels.count == 0)
return {{surface_density, {expolygon}}}; return {{surface_density, {expolygon}}};
// A fixed sampling grid, so that every region of a layer gets the same shells. // A fixed sampling grid, so that every region of a layer gets the same shells.
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef); const coord_t cell = scaled(marchsq::AdaptiveLevelField::gsizef);
BoundingBox bbox = get_extents(expolygon); BoundingBox bbox = get_extents(expolygon);
bbox.offset(cell); bbox.offset(cell);
bbox.merge(align_to_grid(bbox.min, Point(cell, cell))); bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
const marchsq::TpmsLevelField raster(field, levels, bbox, z); const marchsq::AdaptiveLevelField raster(field, levels, bbox, z);
// Each level takes the part deeper than the middle between it and the previous one. // Each level takes the part deeper than the middle between it and the previous one.
std::vector<TpmsShell> shells; std::vector<AdaptiveInfillShell> shells;
ExPolygons remaining{expolygon}; ExPolygons remaining{expolygon};
for (int level = 0; level < levels.count && !remaining.empty(); ++level) { for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
Polygons deeper; Polygons deeper;
@@ -808,12 +808,12 @@ std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPo
return shells; return shells;
} }
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing, void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell) const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
{ {
FillParams shell_params = params; FillParams shell_params = params;
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled; shell_params.adaptive_infill_mode = AdaptiveInfillMode::Disabled;
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) { for (const AdaptiveInfillShell &shell : make_adaptive_infill_shells(field, expolygon, z, params.density, params.adaptive_infill_interior_density, params.adaptive_infill_gradient)) {
shell_params.density = shell.density; shell_params.density = shell.density;
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing)))) for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
fill_shell(shell_params, part); fill_shell(shell_params, part);
@@ -21,7 +21,7 @@ namespace Slic3r {
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes, // lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The // every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes. // modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
class TpmsRadialField class AdaptiveInfillField
{ {
public: public:
struct Slice struct Slice
@@ -39,7 +39,7 @@ public:
}; };
// Slices sorted by z, in the XY coordinates of the fill and the print Z. // Slices sorted by z, in the XY coordinates of the fill and the print Z.
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode, AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
const std::function<void()> &throw_if_canceled); const std::function<void()> &throw_if_canceled);
// Lobes blended near the sides between them, from a body or from each of the two sections around a point. // Lobes blended near the sides between them, from a body or from each of the two sections around a point.
@@ -56,7 +56,7 @@ public:
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D. // Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
int axis() const { return m_axis; } int axis() const { return m_axis; }
TpmsAdaptiveMode mode() const { return m_mode; } AdaptiveInfillMode mode() const { return m_mode; }
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at // In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
// the surface to 1. // the surface to 1.
@@ -91,7 +91,7 @@ private:
static constexpr int Polar = 24; static constexpr int Polar = 24;
static constexpr int Azimuth = 48; static constexpr int Azimuth = 48;
TpmsAdaptiveMode m_mode; AdaptiveInfillMode m_mode;
int m_axis; int m_axis;
Vec3d m_origin; Vec3d m_origin;
double m_cell; double m_cell;
@@ -106,9 +106,9 @@ private:
std::vector<float> m_depth; std::vector<float> m_depth;
}; };
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>; using AdaptiveInfillFieldPtr = std::unique_ptr<AdaptiveInfillField>;
// A field for every adaptive mode in use, indexed by the mode. // A field for every adaptive mode in use, indexed by the mode.
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>; using AdaptiveInfillFields = std::array<AdaptiveInfillFieldPtr, size_t(AdaptiveInfillMode::Count)>;
struct AdaptiveTpms struct AdaptiveTpms
{ {
@@ -117,14 +117,14 @@ struct AdaptiveTpms
// Pattern frequencies at the surface and at the center, in radians per mm. // Pattern frequencies at the surface and at the center, in radians per mm.
double surface_frequency; double surface_frequency;
double interior_frequency; double interior_frequency;
TpmsAdaptiveGradient gradient; AdaptiveInfillGradient gradient;
}; };
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer. // Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox, Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle); coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
struct TpmsShell struct AdaptiveInfillShell
{ {
float density; float density;
ExPolygons expolygons; ExPolygons expolygons;
@@ -132,12 +132,12 @@ struct TpmsShell
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the // Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
// middle of the layer. // middle of the layer.
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient); float surface_density, float interior_density, AdaptiveInfillGradient gradient);
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region, // Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
// so the lines connected along the boundaries of two shells don't overlap. // so the lines connected along the boundaries of two shells don't overlap.
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing, void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell); const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r } // namespace Slic3r
+23 -23
View File
@@ -312,9 +312,9 @@ struct SurfaceFillParams
bool gyroid_optimized = false; bool gyroid_optimized = false;
// For TPMS: grade the density from the surface to the interior of the object. // For TPMS: grade the density from the surface to the interior of the object.
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled; AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
float tpms_interior_density = 0.f; float adaptive_infill_interior_density = 0.f;
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear; AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
// Orca: corner smoothing factor in the range [0, 1]. // Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. }; double smooth_factor { 0. };
@@ -358,9 +358,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized); RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive); RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
RETURN_COMPARE_NON_EQUAL(tpms_interior_density); RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient); RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
RETURN_COMPARE_NON_EQUAL(smooth_factor); RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern); RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills); RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -394,9 +394,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern && this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills && this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized && this->gyroid_optimized == rhs.gyroid_optimized &&
this->tpms_adaptive == rhs.tpms_adaptive && this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
this->tpms_interior_density == rhs.tpms_interior_density && this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient && this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
this->smooth_factor == rhs.smooth_factor && this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order; this->fill_order == rhs.fill_order;
} }
@@ -1010,17 +1010,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface; params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused. // Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ? params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled; region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled; const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f; params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear; params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
// Pass through gyroid_optimized only when the effective pattern is Gyroid, // Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag // so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
// (which would unnecessarily split fill batching). Adaptive density replaces it. // (which would unnecessarily split fill batching). Adaptive density replaces it.
// Stored on SurfaceFillParams; copied to FillParams during conversion. // Stored on SurfaceFillParams; copied to FillParams during conversion.
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive; params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
if (params.extrusion_role == erInternalInfill) { if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value, params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1373,7 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->angle = surface_fill.params.angle; f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle; f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive); f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->print_config = &this->object()->print()->config(); f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config(); f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) { if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1423,9 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2; params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized; params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.tpms_adaptive = surface_fill.params.tpms_adaptive; params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density); params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient; params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.smooth_factor = surface_fill.params.smooth_factor; params.smooth_factor = surface_fill.params.smooth_factor;
// BBS // BBS
@@ -1598,7 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->angle = surface_fill.params.angle; f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle; f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive); f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->print_config = &this->object()->print()->config(); f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config(); f->print_object_config = &this->object()->config();
@@ -1638,9 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline; params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized; params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.tpms_adaptive = surface_fill.params.tpms_adaptive; params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density); params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient; params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.smooth_factor = surface_fill.params.smooth_factor; params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns. // Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds // Without the sparse extrusion role, the filler uses each surface's bounds
+256 -13
View File
@@ -11,6 +11,10 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <utility> #include <utility>
#include <unordered_map>
#include <unordered_set>
#include <tuple>
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp" #include "../ClipperUtils.hpp"
#include "../EdgeGrid.hpp" #include "../EdgeGrid.hpp"
#include "../Geometry.hpp" #include "../Geometry.hpp"
@@ -206,6 +210,9 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
thick_polylines.clear(); thick_polylines.clear();
} }
else { 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( extrusion_entities_append_paths(
eec->entities, std::move(polylines), eec->entities, std::move(polylines),
params.extrusion_role, params.extrusion_role,
@@ -1644,6 +1651,8 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
}; };
const double line_half_width = 0.5 * scale_(spacing); const double line_half_width = 0.5 * scale_(spacing);
// Two ends whose anchors would meet are joined instead, with about as much wall.
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
#if 0 #if 0
// Connection from end of one infill line to the start of another infill line. // Connection from end of one infill line to the start of another infill line.
@@ -1720,6 +1729,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
} }
#endif #endif
// Line ends not yet joined to another line along the contour.
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
struct Arc { struct Arc {
ContourIntersectionPoint *intersection; ContourIntersectionPoint *intersection;
double arc_length; double arc_length;
@@ -1731,6 +1744,81 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next()) if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) }); arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; }); std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
// Lines the arcs would join in this order.
auto count_joins = [&](const std::vector<Arc> &order) {
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
for (size_t i = 0; i < consumed.size(); ++ i)
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
std::vector<size_t> parent(infill_ordered.size());
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
size_t joins = 0;
for (const Arc &arc : order) {
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
continue;
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
if (polyline1 == polyline2)
continue;
if (arc.arc_length < join_length) {
parent[polyline2] = polyline1;
++ joins;
} else if (anchor_length <= SCALED_EPSILON)
continue;
consumed[end1] = consumed[end2] = 1;
}
// The last pass joins the open ends next to each other, also those of a multiline group.
for (size_t end = 0; end < consumed.size(); ++ end) {
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
continue;
const double length = graph.boundary_params[cp.contour_idx].back();
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
};
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
consumed[end] = anchor_length > SCALED_EPSILON;
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l > join_length)
break;
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
if (polyline1 == polyline2)
continue;
parent[polyline2] = polyline1;
consumed[end] = consumed[other] = 1;
++ joins;
break;
}
}
return joins;
};
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
size_t best_joins = count_joins(arches);
const double bucket = 0.5 * scale_(spacing);
for (const bool backwards : { false, true }) {
std::vector<Arc> along_contour = arches;
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
if (bucket_l != bucket_r)
return bucket_l < bucket_r;
if (l.intersection->contour_idx != r.intersection->contour_idx)
return l.intersection->contour_idx < r.intersection->contour_idx;
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
});
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
best_joins = joins;
arches = std::move(along_contour);
}
}
} }
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization. //FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
@@ -1752,7 +1840,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (polyline_idx1 != polyline_idx2) { if (polyline_idx1 != polyline_idx2) {
Polyline &polyline1 = infill_ordered[polyline_idx1]; Polyline &polyline1 = infill_ordered[polyline_idx1];
Polyline &polyline2 = infill_ordered[polyline_idx2]; Polyline &polyline2 = infill_ordered[polyline_idx2];
if (arc.arc_length < anchor_length_max) { if (arc.arc_length < join_length) {
// Not closing a loop, connecting the lines. // Not closing a loop, connecting the lines.
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back()); assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
if (contour[cp1->point_idx] == polyline1.points.front()) if (contour[cp1->point_idx] == polyline1.points.front())
@@ -1761,6 +1849,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back()) if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse(); polyline2.reverse();
take(polyline1, polyline2, contour, cp1, cp2, false); 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. // Mark the second polygon as merged with the first one.
if (polyline_idx2 < polyline_idx1) { if (polyline_idx2 < polyline_idx1) {
polyline2 = std::move(polyline1); polyline2 = std::move(polyline1);
@@ -1778,6 +1867,23 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
} }
} }
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
const double length = graph.boundary_params[from.contour_idx].back();
double distance = 0.;
const ContourIntersectionPoint *cp = &from;
for (size_t step = 0; step < 64; ++ step) {
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
if (other == nullptr || other == &from)
break;
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
return distance;
cp = other;
}
return std::numeric_limits<double>::max();
};
// Connect the remaining open infill lines to the perimeter lines if possible. // Connect the remaining open infill lines to the perimeter lines if possible.
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary) for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) { if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
@@ -1796,7 +1902,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back()); assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
bool connected = false; bool connected = false;
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) { for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l == std::numeric_limits<double>::max() || l > anchor_length_max) if (l == std::numeric_limits<double>::max() || l > join_length)
break; break;
// Take the complete contour. // Take the complete contour.
bool reversed = l == lprev; bool reversed = l == lprev;
@@ -1815,6 +1921,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back()) if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse(); polyline2.reverse();
take(polyline, polyline2, contour, &contour_point, cp2, reversed); 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) { if (polyline_idx < polyline_idx2) {
// Mark the second polyline as merged with the first one. // Mark the second polyline as merged with the first one.
merged_with[polyline_idx2] = polyline_idx; merged_with[polyline_idx2] = polyline_idx;
@@ -1829,18 +1936,35 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
break; break;
} }
if (! connected && anchor_length > SCALED_EPSILON) { if (! connected && anchor_length > SCALED_EPSILON) {
// Which to take? One could optimize for: // Hook toward the nearest open end of another polyline, where the next path likely starts.
// 1) Shortest path const double free_prev = contour_point.contour_not_taken_length_prev;
// 2) Hook length const double free_next = contour_point.contour_not_taken_length_next;
// ... const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
// 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. const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next); bool backwards = free_prev > free_next;
if (l > SCALED_EPSILON) { if (to_prev < to_next && free_prev > SCALED_EPSILON)
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next) backwards = true;
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width); else if (to_next < to_prev && free_next > SCALED_EPSILON)
else backwards = false;
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width); // Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
double length = anchor_length;
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
params.multiline > 1 && other != &contour_point) {
const Point &end = contour[contour_point.point_idx];
Vec2d dir = Vec2d::Zero();
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
}
} }
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
backwards, std::max(0., length), line_half_width);
} }
} }
@@ -2798,4 +2922,123 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
polylines = std::move(all_polylines); polylines = std::move(all_polylines);
} }
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
{
const size_t n = polylines.size();
if (n < 2 || max_gap <= 0.)
return;
static constexpr size_t npos = size_t(-1);
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
std::unordered_map<uint64_t, std::vector<size_t>> grid;
for (size_t e = 0; e < 2 * n; ++ e)
if (polylines[e >> 1].size() >= 2)
grid[key(cell_of(end_point(e)))].push_back(e);
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
std::vector<char> backwards(n, 0);
for (size_t i = 0; i < n; ++ i)
if (polylines[i].size() >= 2) {
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
}
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
for (const auto &[cell_key, ends] : grid) {
const Vec2i64 c = cell_of(end_point(ends.front()));
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t a : ends)
for (size_t b : it->second)
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
}
if (gaps.empty())
return;
std::sort(gaps.begin(), gaps.end());
std::vector<size_t> partner(2 * n, npos), parent(n);
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
// Only the segments through a cell next to a candidate join can cross one.
std::unordered_set<uint64_t> near_gaps;
for (const auto &[apart, trail, d, a, b] : gaps)
for (size_t e : { a, b })
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
Lines near_lines;
for (const Polyline &pl : polylines)
for (size_t i = 1; i < pl.size(); ++ i) {
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
bool near = (hi - lo).maxCoeff() > 3;
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
if (near)
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
}
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
auto crosses = [&](size_t a, size_t b) {
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
const double length = (q - p).norm();
if (length <= 2. * SCALED_EPSILON)
return false;
const Vec2d dir = (q - p) / length;
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
return true;
const Vec2i64 c = cell_of(end_point(a));
for (int64_t dx = -3; dx <= 3; ++ dx)
for (int64_t dy = -3; dy <= 3; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t e : it->second)
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
return true;
return false;
};
for (const auto &[apart, trail, d, a, b] : gaps)
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
partner[a] = b;
partner[b] = a;
parent[find(a >> 1)] = find(b >> 1);
}
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
Polylines out;
std::vector<char> done(n, 0);
auto walk = [&](size_t first_end) {
Polyline joined;
for (size_t e = first_end; e != npos;) {
Polyline &pl = polylines[e >> 1];
done[e >> 1] = 1;
if (e & 1)
pl.reverse();
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
e = partner[e ^ 1];
}
out.emplace_back(std::move(joined));
};
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i] == npos)
walk(2 * i);
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i + 1] == npos)
walk(2 * i + 1);
polylines = std::move(out);
}
} // namespace Slic3r } // namespace Slic3r
+9 -6
View File
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r { namespace Slic3r {
class Surface; class Surface;
class TpmsRadialField; class AdaptiveInfillField;
enum InfillPattern : int; enum InfillPattern : int;
namespace FillAdaptive { namespace FillAdaptive {
@@ -93,9 +93,9 @@ struct FillParams
bool gyroid_optimized { false }; bool gyroid_optimized { false };
// For TPMS: grade the density from the surface to the interior of the object. Density fraction. // For TPMS: grade the density from the surface to the interior of the object. Density fraction.
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled }; AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
float tpms_interior_density { 0.f }; float adaptive_infill_interior_density { 0.f };
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear }; AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
// Orca: corner smoothing factor in the range [0, 1]. // Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. }; double smooth_factor { 0. };
@@ -162,7 +162,7 @@ public:
FillAdaptive::Octree* adapt_fill_octree = nullptr; FillAdaptive::Octree* adapt_fill_octree = nullptr;
// Radial coordinate inside the object for the adaptive TPMS infill // Radial coordinate inside the object for the adaptive TPMS infill
const TpmsRadialField* tpms_radial_field = nullptr; const AdaptiveInfillField* adaptive_infill_field = nullptr;
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring). // PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function. // Orca: also used by gap fill function.
@@ -258,8 +258,11 @@ public:
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance); 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); 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 } // namespace Slic3r
#endif // slic3r_FillBase_hpp_ #endif // slic3r_FillBase_hpp_
+6 -6
View File
@@ -18,7 +18,7 @@
#include "FillGyroid.hpp" #include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
namespace Slic3r { namespace Slic3r {
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon, ExPolygon expolygon,
Polylines &polylines_out) Polylines &polylines_out)
{ {
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) { [&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
}); });
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
// generate pattern // generate pattern
Polylines polylines; Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
// Radians per mm of the regular pattern at a density. // Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); }; auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient}, polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle); *this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) { } else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches // Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj. // the standard parametric path's wavelength: 2*pi * spacing / density_adj.
+6 -6
View File
@@ -17,7 +17,7 @@
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp" #include "FillTpmsD.hpp"
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
namespace Slic3r { namespace Slic3r {
@@ -117,8 +117,8 @@ void FillTpmsD::_fill_surface_single(
ExPolygon expolygon, ExPolygon expolygon,
Polylines &polylines_out) Polylines &polylines_out)
{ {
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) { [&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
}); });
@@ -130,13 +130,13 @@ void FillTpmsD::_fill_surface_single(
expolygon.rotate(-infill_angle); expolygon.rotate(-infill_angle);
Polylines polylines; Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
// Radians per mm of the regular pattern at a density. // Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); }; auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
BoundingBox bbox = expolygon.contour.bounding_box(); BoundingBox bbox = expolygon.contour.bounding_box();
bbox.offset(scale_((params.multiline + 1) * this->spacing)); bbox.offset(scale_((params.multiline + 1) * this->spacing));
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient}, polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle); *this->adaptive_infill_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
} else { } else {
BoundingBox bb = expolygon.contour.bounding_box(); BoundingBox bb = expolygon.contour.bounding_box();
// Density adjusted to have a good %of weight. // Density adjusted to have a good %of weight.
+7 -7
View File
@@ -8,7 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp" #include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp" #include "FillTpmsFK.hpp"
#include "FillTpmsAdaptive.hpp" #include "AdaptiveInfillField.hpp"
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
@@ -132,8 +132,8 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
ExPolygon expolygon, ExPolygon expolygon,
Polylines& polylines_out) Polylines& polylines_out)
{ {
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing, fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) { [&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out); this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
}); });
@@ -151,10 +151,10 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges. // Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing)); bbox.offset(scale_((params.multiline + 1) * spacing));
Polylines polylines; Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) { if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density), polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.adaptive_infill_interior_density),
params.tpms_adaptive_gradient}, params.adaptive_infill_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle); *this->adaptive_infill_field, bbox, this->z, params.layer_height, spacing, infill_angle);
} else { } else {
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density)); marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
// Get simplified lines using coarse tolerance of 0.1mm (this is infill). // Get simplified lines using coarse tolerance of 0.1mm (this is infill).
-8
View File
@@ -6660,10 +6660,6 @@ LayerResult GCode::process_layer(
case CalibMode::Calib_Input_shaping_freq: { case CalibMode::Calib_Input_shaping_freq: {
if (m_layer_index == 1){ if (m_layer_index == 1){
gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type); 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 { } else {
if (print.calib_params().freqStartX == print.calib_params().freqStartY && print.calib_params().freqEndX == print.calib_params().freqEndY) { 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), ""); 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: { case CalibMode::Calib_Input_shaping_damp: {
if (m_layer_index == 1){ 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('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); gcode += writer().set_input_shaping('Y', 0.f, print.calib_params().freqStartY, print.calib_params().shaper_type);
} else { } else {
+293 -100
View File
@@ -24,6 +24,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/classification.hpp> #include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/constants.hpp> #include <boost/algorithm/string/constants.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
@@ -378,6 +379,11 @@ void GCodeProcessor::TimeMachine::reset()
travel_acceleration = 0.0f; travel_acceleration = 0.0f;
max_travel_acceleration = 0.0f; max_travel_acceleration = 0.0f;
extrude_factor_override_percentage = 1.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; time = 0.0f;
stop_times = std::vector<StopTime>(); stop_times = std::vector<StopTime>();
curr.reset(); curr.reset();
@@ -493,16 +499,87 @@ GCodeProcessor::TimeMachine::AdditionalBuffer GCodeProcessor::TimeMachine::merge
return merged; 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) 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) if (!enabled)
return; return;
// Orca: on the finalization pass, drain extra time that is still buffered (e.g. a // Drain buffered delays on the final pass, even with no motion left.
// trailing filament change) even with fewer than two blocks queued -- no later pass // A trailing filament change has no later move to receive its time.
// 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).
const bool drain_final = is_final && !m_additional_time_buffer.empty(); 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 // Not enough blocks to attribute the extra time to yet; buffer it so it is
// applied on a later pass instead of being dropped. // applied on a later pass instead of being dropped.
if (additional_time > 0.0f) if (additional_time > 0.0f)
@@ -510,7 +587,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
return; 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 // 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 // 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.emplace_back(target_move_type, additional_time);
additional_buffer = merge_adjacent_additional_time_blocks(additional_buffer); additional_buffer = merge_adjacent_additional_time_blocks(additional_buffer);
// reverse_pass size_t n_blocks_process;
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i) { if (klipper) {
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]); n_blocks_process = plan_klipper(keep_last_n_blocks != 0);
keep_last_n_blocks = blocks.size() - n_blocks_process;
} else {
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i)
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
for (size_t i = 0; i + 1 < blocks.size(); ++i)
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
recalculate_trapezoids(blocks);
n_blocks_process = blocks.size() - keep_last_n_blocks;
} }
// forward_pass
for (size_t i = 0; i + 1 < blocks.size(); ++i) {
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
}
recalculate_trapezoids(blocks);
const size_t n_blocks_process = blocks.size() - keep_last_n_blocks;
size_t additional_buffer_idx = 0; size_t additional_buffer_idx = 0;
for (size_t i = 0; i < n_blocks_process; ++i) { for (size_t i = 0; i < n_blocks_process; ++i) {
const TimeBlock& block = blocks[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()) { if (additional_buffer_idx < additional_buffer.size()) {
const EMoveType buf_move_type = additional_buffer[additional_buffer_idx].first; const EMoveType buf_move_type = additional_buffer[additional_buffer_idx].first;
if (buf_move_type == EMoveType::Noop || buf_move_type == block.move_type) { 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 // 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 // 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 // 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. // reader until the pre-heat injector consumes it, so output is byte-identical.
if (block.skippable_type != SkipType::stNone) if (block.skippable_type != SkipType::stNone)
result.skippable_part_time[block.skippable_type] += block.time(); result.skippable_part_time[block.skippable_type] += motion_time;
result.moves[block.move_id].time[static_cast<size_t>(mode)] = block_time; result.moves[block.move_id].time[static_cast<size_t>(mode)] = block_time;
gcode_time.cache += block_time; gcode_time.cache += block_time;
//BBS //BBS
@@ -689,14 +779,12 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
if (keep_last_n_blocks) { if (keep_last_n_blocks) {
blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process); blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process);
// Ensure that the new first block's entry speed will be preserved to prevent discontinuity // Preserve the boundary speed in the Marlin planner. Klipper only flushes
// between the erased blocks' exit speed and the new first block's entry speed. // moves whose cruise speed is already independent of future moves.
// Otherwise, the first block's entry speed could be recalculated on the next pass without if (!klipper) {
// considering that there are no more blocks before this first block. This could lead TimeBlock& first_block = blocks.front();
// to discontinuity between the exit speed (of already processed blocks) and the entry first_block.max_entry_speed = first_block.feedrate_profile.entry;
// speed of the first block. }
TimeBlock &first_block = blocks.front();
first_block.max_entry_speed = first_block.feedrate_profile.entry;
} else { } else {
blocks.clear(); blocks.clear();
} }
@@ -3872,9 +3960,9 @@ void GCodeProcessor::finalize(bool post_process)
update_slice_warnings(); update_slice_warnings();
} }
float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const double GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
{ {
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? float(m_time_processor.machines[static_cast<size_t>(mode)].time) : 0.0f; return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast<size_t>(mode)].time : 0.0;
} }
float GCodeProcessor::get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const 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(); if (line.has_e()) g1_axes[E] = (double)line.e();
std::optional<double> g1_feedrate = std::nullopt; std::optional<double> g1_feedrate = std::nullopt;
if (line.has_f()) g1_feedrate = (double)line.f(); if (line.has_f()) g1_feedrate = (double)line.f();
if (m_flavor == gcfKlipper) {
// Preserve decimal coordinates for short segments without parsing twice.
for (unsigned char axis = X; axis <= E; ++axis)
if (line.has(static_cast<Axis>(axis)))
g1_axes[axis] = line.precise_value(static_cast<Axis>(axis));
if (line.has_f())
g1_feedrate = line.precise_value(F);
}
process_G1(g1_axes, g1_feedrate); process_G1(g1_axes, g1_feedrate);
} }
@@ -5228,7 +5324,7 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
//BBS: limite the cruise according to centripetal acceleration //BBS: limite the cruise according to centripetal acceleration
//Only need to handle when both prev and curr segment has movement in x-y plane //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)) { (curr.enter_direction(0) != 0.0f || curr.enter_direction(1) != 0.0f)) {
Vec3f v1 = prev.exit_direction; Vec3f v1 = prev.exit_direction;
v1(2, 0) = 0.0f; v1(2, 0) = 0.0f;
@@ -5252,6 +5348,15 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
} }
} }
if (m_flavor == gcfKlipper && !is_extrusion_only_move(delta_pos)) {
const float max_velocity = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), X, m_machine_config_idx);
if (max_velocity > 0.0f)
curr.feedrate = std::min(curr.feedrate, max_velocity);
}
// Klipper applies E limits to retractions and moves without XY motion,
// not to positive extrusion along a printing path.
const bool limit_extruder = m_flavor != gcfKlipper || delta_pos[E] < 0.0 ||
(delta_pos[X] == 0.0 && delta_pos[Y] == 0.0);
// calculates block cruise feedrate // calculates block cruise feedrate
float min_feedrate_factor = 1.0f; float min_feedrate_factor = 1.0f;
for (unsigned char a = X; a <= E; ++a) { for (unsigned char a = X; a <= E; ++a) {
@@ -5260,7 +5365,8 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage; curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage;
curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]); curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]);
if (curr.abs_axis_feedrate[a] != 0.0f) { if (curr.abs_axis_feedrate[a] != 0.0f && (a != E || limit_extruder) &&
(m_flavor != gcfKlipper || a == Z || a == E)) {
float axis_max_feedrate = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx); float axis_max_feedrate = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min<float>(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]); if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min<float>(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]);
} }
@@ -5277,20 +5383,42 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
} }
// calculates block acceleration // calculates block acceleration
float acceleration = float acceleration;
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) : if (m_flavor == gcfKlipper && is_extrusion_only_move(delta_pos)) {
(is_extrusion_only_move(delta_pos) ? // Klipper's max_extrude_only_accel. machine_max_acceleration_e is optional and some
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) : // shipped profiles leave it at 0, so keep the non-zero retract fallback.
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i))); acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), E, m_machine_config_idx);
if (acceleration <= 0.0f)
acceleration = get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i));
} else
acceleration =
(m_flavor == gcfKlipper) ? get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(is_extrusion_only_move(delta_pos) ?
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
//BBS //BBS
for (unsigned char a = X; a <= E; ++a) { for (unsigned char a = X; a <= E; ++a) {
if ((a == E && !limit_extruder) || (m_flavor == gcfKlipper && (a == X || a == Y)))
continue;
float axis_max_acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx); float axis_max_acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
// An unset (0) limit means "no limit", as in the feedrate loop above. Only the Klipper
// planner divides by the block acceleration, so leave the Marlin path as it was.
if (m_flavor == gcfKlipper && axis_max_acceleration <= 0.0f)
continue;
if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration) if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration)
acceleration = axis_max_acceleration / (std::abs(delta_pos[a]) * inv_distance); acceleration = axis_max_acceleration / (std::abs(delta_pos[a]) * inv_distance);
} }
block.acceleration = acceleration; block.acceleration = acceleration;
if (m_flavor == gcfKlipper) {
block.junction_deviation = get_junction_deviation(static_cast<PrintEstimatedStatistics::ETimeMode>(i), acceleration);
const float cruise_acceleration = machine.requested_accel_to_decel >= 0.0f ?
std::min(machine.acceleration, machine.requested_accel_to_decel) :
machine.acceleration * (1.0f - machine.minimum_cruise_ratio);
block.mcr_delta_v2 = 2.0f * distance * std::min(acceleration, cruise_acceleration);
}
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f; static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD; const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
@@ -5401,6 +5529,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
block.feedrate_profile.entry = std::min(vmax_junction, v_allowable); block.feedrate_profile.entry = std::min(vmax_junction, v_allowable);
block.max_entry_speed = vmax_junction; 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.nominal_length = (block.feedrate_profile.cruise <= v_allowable);
block.flags.recalculate = true; block.flags.recalculate = true;
block.safe_feedrate = curr.safe_feedrate; block.safe_feedrate = curr.safe_feedrate;
@@ -5412,9 +5546,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
prev = curr; prev = curr;
blocks.push_back(block); 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); calculate_time(m_result, TimeProcessor::Planner::queue_size);
const Vec3f plate_offset = {(float) m_x_offset, (float) m_y_offset, 0.0f}; 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) void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{ {
if (m_flavor == gcfKlipper)
simulate_st_synchronize();
int filament_id = get_filament_id(); int filament_id = get_filament_id();
float new_temp; float new_temp;
if (line.has_value('R', 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) void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
{ {
if (m_flavor == gcfKlipper)
simulate_st_synchronize();
float new_temp; float new_temp;
if (line.has_value('S', 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; 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) void GCodeProcessor::process_M204(const GCodeReader::GCodeLine& line)
{ {
float value; float value;
if (m_flavor == gcfKlipper) {
if (!line.has_value('S', value)) {
float travel;
if (!line.has_value('P', value) || !line.has_value('T', travel))
return;
value = std::min(value, travel);
}
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
// setters would diverge from the firmware when custom G-code raises the limit.
if (value > 0.0f) {
for (auto& machine : m_time_processor.machines)
machine.acceleration = machine.travel_acceleration = value;
}
return;
}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) { for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal || if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal ||
m_time_processor.machine_envelope_processing_enabled) { 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) void GCodeProcessor::process_SET_VELOCITY_LIMIT(const GCodeReader::GCodeLine& line)
{ {
// handle SQUARE_CORNER_VELOCITY static const std::regex parameter_pattern(
static const std::regex square_corner_velocity_pattern("\\sSQUARE_CORNER_VELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)"); 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::smatch matches; std::regex::icase);
if (std::regex_search(line.raw(), matches, square_corner_velocity_pattern) && matches.size() == 2) { const std::string command = line.raw().substr(0, line.raw().find(';'));
float _jerk = 0; std::vector<std::pair<std::string, float>> parameters;
try for (std::sregex_iterator it(command.begin(), command.end(), parameter_pattern), end; it != end; ++it) {
{ float value;
_jerk = std::stof(matches[1]); try {
value = std::stof((*it)[2].str());
} catch (const std::exception&) {
continue;
} }
catch (...){} if (std::isfinite(value))
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) { parameters.emplace_back(boost::to_upper_copy((*it)[1].str()), value);
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); // Klipper 0.13 converts ACCEL_TO_DECEL only when the line has no MINIMUM_CRUISE_RATIO, and later
// releases ignore it, so a line carrying both (as Orca writes) is planned with the cruise ratio.
const bool has_cruise_ratio = std::any_of(parameters.begin(), parameters.end(),
[](const auto& parameter) { return parameter.first == "MINIMUM_CRUISE_RATIO" && parameter.second >= 0.0f && parameter.second < 1.0f; });
for (const auto& [name, value] : parameters) {
for (size_t i = 0; i < m_time_processor.machines.size(); ++i) {
TimeMachine& machine = m_time_processor.machines[i];
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
// setters would diverge from the firmware when custom G-code raises the limit.
if (name == "ACCEL" && value > 0.0f)
machine.acceleration = machine.travel_acceleration = value;
else if (name == "MINIMUM_CRUISE_RATIO" && value >= 0.0f && value < 1.0f) {
if (machine.klipper_queue_priming && machine.requested_accel_to_decel >= 0.0f)
machine.klipper_junction_flush -= 1.0f;
machine.minimum_cruise_ratio = value;
machine.requested_accel_to_decel = -1.0f;
} else if (name == "ACCEL_TO_DECEL" && value > 0.0f && !has_cruise_ratio) {
if (machine.klipper_queue_priming && machine.requested_accel_to_decel < 0.0f)
machine.klipper_junction_flush += 1.0f;
machine.requested_accel_to_decel = value;
} else if (name == "SQUARE_CORNER_VELOCITY" && value >= 0.0f) {
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, value);
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, value);
} else if (name == "VELOCITY" && value > 0.0f) {
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, value);
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, value);
}
} }
} }
static const std::regex accel_pattern("\\sACCEL\\s*=\\s*([0-9]*\\.*[0-9]*)");
if (std::regex_search(line.raw(), matches, accel_pattern) && matches.size() == 2) {
float _accl = 0;
try
{
_accl = std::stof(matches[1]);
}
catch (...) {}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
set_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
}
}
static const std::regex velocity_pattern("\\sVELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
if (std::regex_search(line.raw(), matches, velocity_pattern) && matches.size() == 2) {
float _speed = 0;
try
{
_speed = std::stof(matches[1]);
}
catch (...) {}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, _speed);
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, _speed);
}
}
} }
void GCodeProcessor::process_M221(const GCodeReader::GCodeLine& line) 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_s = 0.0;
float value_p = 0.0; float value_p = 0.0;
if (line.has_value('S', value_s) || line.has_value('P', value_p)) { const bool has_delay = line.has_value('S', value_s) || line.has_value('P', value_p);
value_s += value_p * 0.001; if (m_flavor != gcfKlipper && !has_delay)
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see return;
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here. value_s += value_p * 0.001;
if (m_skip_end_gcode_delays) // Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
return; // m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
simulate_st_synchronize(value_s); if (m_skip_end_gcode_delays && has_delay)
} return;
simulate_st_synchronize(value_s);
} }
void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line) void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line)
@@ -7460,12 +7617,12 @@ float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode
{ {
const size_t id = static_cast<size_t>(mode); const size_t id = static_cast<size_t>(mode);
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel // Klipper derives junction deviation from the live toolhead acceleration,
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel // before a move is constrained by Z or extruder limits.
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
if (m_flavor == gcfKlipper) { if (m_flavor == gcfKlipper) {
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it. // 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); 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) if (scv <= 0.0f || acceleration <= 0.0f)
return 0.0f; return 0.0f;
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration; 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, const TimeMachine::State& curr, bool has_prev_move,
PrintEstimatedStatistics::ETimeMode mode) const PrintEstimatedStatistics::ETimeMode mode) const
{ {
if (m_flavor == gcfKlipper) {
if (!has_prev_move || curr.enter_direction.isZero() || prev.exit_direction.isZero())
return 0.0f;
const TimeBlock& previous = m_time_processor.machines[static_cast<size_t>(mode)].blocks.back();
double limit = std::min(sqr(double(block.feedrate_profile.cruise)), sqr(double(prev.feedrate)));
const double extrude_ratio_change = std::abs(curr.axis_feedrate[E] / curr.feedrate -
prev.axis_feedrate[E] / prev.feedrate);
// Klipper's instantaneous_corner_velocity defaults to 1 mm/s.
// It is independent of machine_max_jerk_e; custom firmware values
// cannot be inferred from the slicer's jerk setting.
if (extrude_ratio_change > 0.0f)
limit = std::min(limit, sqr(1.0 / extrude_ratio_change));
// Recover directions from the double-precision axis velocities: float
// unit vectors lose the angle between nearly collinear short segments.
const Vec3d prev_direction = Vec3d(prev.axis_feedrate[X], prev.axis_feedrate[Y], prev.axis_feedrate[Z]).normalized();
const Vec3d curr_direction = Vec3d(curr.axis_feedrate[X], curr.axis_feedrate[Y], curr.axis_feedrate[Z]).normalized();
const double cosine = std::clamp(-prev_direction.dot(curr_direction), -1.0, 1.0);
const double sin_half = std::sqrt(0.5 * (1.0 - cosine));
const double cos_half = std::sqrt(0.5 * (1.0 + cosine));
if (sin_half < 1.0 && cos_half > 0.0) {
const double radius = sin_half / (1.0 - sin_half);
const double half_tan = 0.5 * sin_half / cos_half;
limit = std::min({limit,
radius * block.junction_deviation * block.acceleration,
radius * previous.junction_deviation * previous.acceleration,
half_tan * block.distance * block.acceleration,
half_tan * previous.distance * previous.acceleration});
}
return std::sqrt(limit);
}
const float junction_deviation = get_junction_deviation(mode, block.acceleration); const float junction_deviation = get_junction_deviation(mode, block.acceleration);
if (junction_deviation <= 0.0f) if (junction_deviation <= 0.0f)
return -1.0f; // classic jerk machine, the caller keeps its own computation return -1.0f; // classic jerk machine, the caller keeps its own computation
if (!has_prev_move) if (!has_prev_move)
return 0.0f; // starts from rest, the planner raises this on the reverse pass 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(). // Unit vectors over XYZE keep this a cosine. Scaling by 1 / XYZ distance
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance // leaves an E term that makes extruding corners appear straighter.
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter // Marlin normalizes over XYZE; the Klipper branch above uses XYZ only.
// 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.
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec); float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
if (junction_cos_theta > 0.999999f) if (junction_cos_theta > 0.999999f)
return 0.0f; // the path doubles back, the machine has to stop 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); 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 // 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. // capped by the centripetal acceleration it needs.
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) { if (block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin: // Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html // https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f; const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
@@ -7700,6 +7884,7 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
std::vector<TimeMachine::ActualSpeedMove> actual_speed_moves; std::vector<TimeMachine::ActualSpeedMove> actual_speed_moves;
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) { for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
TimeMachine& machine = m_time_processor.machines[i]; TimeMachine& machine = m_time_processor.machines[i];
machine.klipper = m_flavor == gcfKlipper;
machine.calculate_time(m_result, static_cast<PrintEstimatedStatistics::ETimeMode>(i), keep_last_n_blocks, additional_time, target_move_type, is_final); machine.calculate_time(m_result, static_cast<PrintEstimatedStatistics::ETimeMode>(i), keep_last_n_blocks, additional_time, target_move_type, is_final);
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal) if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal)
actual_speed_moves = std::move(machine.actual_speed_moves); 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) void GCodeProcessor::simulate_st_synchronize(float additional_time, EMoveType target_move_type)
{ {
calculate_time(m_result, 0, additional_time, 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() 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); is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
const double lengthsScaleFactor = (m_units == EUnits::Inches) ? double(INCHES_TO_MM) : 1.0; 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) // if (axis == E && m_use_volumetric_e)
// ret /= area_filament_cross_section; // ret /= area_filament_cross_section;
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret; return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
+16 -3
View File
@@ -91,12 +91,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
struct Mode struct Mode
{ {
float time; double time;
float prepare_time; float prepare_time;
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> custom_gcode_times; std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> custom_gcode_times;
void reset() { void reset() {
time = 0.0f; time = 0.0;
prepare_time = 0.0f; prepare_time = 0.0f;
custom_gcode_times.clear(); custom_gcode_times.clear();
custom_gcode_times.shrink_to_fit(); 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 acceleration{ 0.0f }; // mm/s^2
float max_entry_speed{ 0.0f }; // mm/s float max_entry_speed{ 0.0f }; // mm/s
float safe_feedrate{ 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; Flags flags;
FeedrateProfile feedrate_profile; FeedrateProfile feedrate_profile;
Trapezoid trapezoid; 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. // hard limit for the travel acceleration, to which the firmware will clamp.
float max_travel_acceleration; // mm/s^2 float max_travel_acceleration; // mm/s^2
float extrude_factor_override_percentage; 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 // We accumulate total print time in doubles to reduce the loss of precision
// while adding big floating numbers with small float numbers. // while adding big floating numbers with small float numbers.
double time; // s 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. // Merge adjacent buffer entries that target the same move type.
static AdditionalBuffer merge_adjacent_additional_time_blocks(const AdditionalBuffer& buffer); 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 // additional_time is attributed to the first block matching target_move_type
// (EMoveType::Noop matches any block, i.e. the first processed block). // (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); 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 process_buffer(const std::string& buffer);
void finalize(bool post_process); 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; float get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const;
std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const; std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const;
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const; std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const;
@@ -8,6 +8,7 @@
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include <algorithm> #include <algorithm>
#include <cassert>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -153,6 +154,41 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
return improved; return improved;
} }
std::vector<size_t> tsp_exact_cycle(const Points& centers)
{
const size_t n = centers.size();
std::vector<size_t> path(n);
std::iota(path.begin(), path.end(), 0);
if (n <= 3) return path;
// State mask * m + j: the points 1..m of mask visited after point 0, the last one j + 1.
const size_t m = n - 1;
assert(m <= 16);
std::vector<double> cost((size_t(1) << m) * m, std::numeric_limits<double>::max());
std::vector<size_t> from(cost.size(), size_t(-1));
auto dist = [&centers](size_t a, size_t b) { return (centers[a].cast<double>() - centers[b].cast<double>()).norm(); };
for (size_t j = 0; j < m; ++j)
cost[(size_t(1) << j) * m + j] = dist(0, j + 1);
for (size_t mask = 1; mask < (size_t(1) << m); ++mask)
for (size_t j = 0; j < m; ++j)
if (const double c = cost[mask * m + j]; c < std::numeric_limits<double>::max())
for (size_t k = 0; k < m; ++k)
if (!(mask >> k & 1)) {
const size_t next = (mask | size_t(1) << k) * m + k;
if (const double nc = c + dist(j + 1, k + 1); nc < cost[next]) {
cost[next] = nc;
from[next] = mask * m + j;
}
}
const size_t full = (size_t(1) << m) - 1;
size_t best = full * m;
for (size_t j = 1; j < m; ++j)
if (cost[full * m + j] + dist(j + 1, 0) < cost[best] + dist(best % m + 1, 0))
best = full * m + j;
for (size_t s = best, k = n; k-- > 1; s = from[s])
path[k] = s % m + 1;
return path;
}
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers) void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
{ {
size_t pn = path.size(); size_t pn = path.size();
@@ -29,6 +29,9 @@ bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_
// Returns true if any crossing was removed. // Returns true if any crossing was removed.
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers); bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
// The shortest cycle through the points, starting at point 0, by dynamic programming over subsets; for a few points only.
std::vector<size_t> tsp_exact_cycle(const Points& centers);
// Rotate the cycle so the closing edge (last -> first) is minimized. // Rotate the cycle so the closing edge (last -> first) is minimized.
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers); void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
+5 -2
View File
@@ -26,6 +26,7 @@
#include "ClipperUtils.hpp" #include "ClipperUtils.hpp"
#include "libslic3r/Line.hpp" #include "libslic3r/Line.hpp"
#include "LocalesUtils.hpp" #include "LocalesUtils.hpp"
#include "libslic3r/GCodeWriter.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
@@ -1528,8 +1529,10 @@ private:
gcode << "M204 P" << acceleration; gcode << "M204 P" << acceleration;
} }
else if (m_gcode_flavor == gcfKlipper && m_accel_to_decel_enable) { 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; // One line, so Klipper derives the limit from this acceleration rather than the previous one.
gcode << "\nM204 S" << acceleration; 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 { else {
// M204: Set default acceleration // M204: Set default acceleration
+1 -1
View File
@@ -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)) { if (pend != c && is_end_of_word(*pend)) {
// The axis value has been parsed correctly. // The axis value has been parsed correctly.
if (axis != UNKNOWN_AXIS) if (axis != UNKNOWN_AXIS)
gline.m_axis[int(axis)] = float(v); gline.m_axis[int(axis)] = v;
gline.m_mask |= 1 << int(axis); gline.m_mask |= 1 << int(axis);
c = pend; c = pend;
} else } else
+11 -10
View File
@@ -35,7 +35,8 @@ public:
std::string_view axis_pos(char axis) const; std::string_view axis_pos(char axis) const;
void clear() { m_raw.clear(); } void clear() { m_raw.clear(); }
bool has(Axis axis) const { return (m_mask & (1 << int(axis))) != 0; } 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(char axis) const;
bool has_value(char axis, float &value) const; bool has_value(char axis, float &value) const;
// Parse value of an axis from raw string starting at axis_pos. // 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_p() const { return this->has(P); }
bool has_unknown_axis() const { return this->has(UNKNOWN_AXIS); } bool has_unknown_axis() const { return this->has(UNKNOWN_AXIS); }
float x() const { return m_axis[X]; } float x() const { return float(m_axis[X]); }
float y() const { return m_axis[Y]; } float y() const { return float(m_axis[Y]); }
float z() const { return m_axis[Z]; } float z() const { return float(m_axis[Z]); }
float e() const { return m_axis[E]; } float e() const { return float(m_axis[E]); }
float f() const { return m_axis[F]; } float f() const { return float(m_axis[F]); }
// BBS: add I J P axis // BBS: add I J P axis
float i() const { return m_axis[I]; } float i() const { return float(m_axis[I]); }
float j() const { return m_axis[J]; } float j() const { return float(m_axis[J]); }
float p() const { return m_axis[P]; } float p() const { return float(m_axis[P]); }
static bool cmd_is(const std::string &gcode_line, const char *cmd_test) { static bool cmd_is(const std::string &gcode_line, const char *cmd_test) {
const char *cmd = GCodeReader::skip_whitespaces(gcode_line.c_str()); const char *cmd = GCodeReader::skip_whitespaces(gcode_line.c_str());
@@ -100,7 +101,7 @@ public:
} }
private: private:
std::string m_raw; std::string m_raw;
float m_axis[NUM_AXES]; double m_axis[NUM_AXES];
uint32_t m_mask; uint32_t m_mask;
friend class GCodeReader; friend class GCodeReader;
}; };
+13 -8
View File
@@ -453,12 +453,11 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
gcode += separate_travel ? "M204 T" : "M204 P"; gcode += separate_travel ? "M204 T" : "M204 P";
gcode += value; gcode += value;
} else if (FLAVOR_IS(gcfKlipper)) { } else if (FLAVOR_IS(gcfKlipper)) {
gcode.reserve(96); gcode.reserve(128);
gcode += "SET_VELOCITY_LIMIT ACCEL="; gcode += "SET_VELOCITY_LIMIT ACCEL=";
gcode += value; gcode += value;
if (this->config.accel_to_decel_enable) { if (this->config.accel_to_decel_enable) {
gcode += " ACCEL_TO_DECEL="; append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
if (GCodeWriter::full_gcode_comment) if (GCodeWriter::full_gcode_comment)
gcode += " ; adjust ACCEL_TO_DECEL"; gcode += " ; adjust ACCEL_TO_DECEL";
} }
@@ -473,6 +472,14 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
return gcode; 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) std::string GCodeWriter::set_jerk_xy(double jerk)
{ {
if (jerk < 0.01 || is_approx(jerk, m_last_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(); return std::string();
std::string gcode; std::string gcode;
gcode.reserve(96); gcode.reserve(128);
gcode += "SET_VELOCITY_LIMIT"; gcode += "SET_VELOCITY_LIMIT";
if (set_acceleration) { if (set_acceleration) {
gcode += " ACCEL="; gcode += " ACCEL=";
gcode += std::to_string(acceleration); gcode += std::to_string(acceleration);
if (this->config.accel_to_decel_enable) { if (this->config.accel_to_decel_enable)
gcode += " ACCEL_TO_DECEL="; append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
}
m_last_acceleration = acceleration; m_last_acceleration = acceleration;
} }
if (set_jerk) { if (set_jerk) {
+4
View File
@@ -71,6 +71,10 @@ public:
std::string set_jerk_xy(double jerk); std::string set_jerk_xy(double jerk);
// Orca: set acceleration and jerk in one command for Klipper // Orca: set acceleration and jerk in one command for Klipper
std::string set_accel_and_jerk(unsigned int acceleration, double jerk); 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_junction_deviation(double junction_deviation);
std::string set_pressure_advance(double pa, int tool = -1) const; 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; std::string set_input_shaping(char axis, float damp, float freq, std::string type) const;
+2 -2
View File
@@ -443,8 +443,8 @@ coordf_t Layer::get_sparse_infill_max_void_area()
if (density == 0.) if (density == 0.)
return -1; return -1;
// Orca: the adaptive TPMS infill is as sparse as its interior density. // Orca: the adaptive TPMS infill is as sparse as its interior density.
if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern)) if (density < 100.f && config.adaptive_infill_mode != AdaptiveInfillMode::Disabled && is_adaptive_infill_pattern(pattern))
density = std::min(density, std::max(1.f, float(config.tpms_interior_density))); density = std::min(density, std::max(1.f, float(config.adaptive_infill_interior_density)));
//BBS: rough estimation and need to be optimized //BBS: rough estimation and need to be optimized
double spacing = flow.scaled_spacing() * (100 - density) / density; double spacing = flow.scaled_spacing() * (100 - density) / density;
+3 -3
View File
@@ -1183,9 +1183,9 @@ static std::vector<std::string> s_Preset_print_options{
"is_infill_first", "is_infill_first",
"sparse_infill_density", "sparse_infill_density",
"fill_multiline", "fill_multiline",
"tpms_adaptive", "adaptive_infill_mode",
"tpms_interior_density", "adaptive_infill_interior_density",
"tpms_adaptive_gradient", "adaptive_infill_gradient",
"gyroid_optimized", "gyroid_optimized",
"sparse_infill_pattern", "sparse_infill_pattern",
"sparse_infill_smooth_factor", "sparse_infill_smooth_factor",
+4 -4
View File
@@ -12,7 +12,7 @@
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp" #include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp" #include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp" #include "Fill/AdaptiveInfillField.hpp"
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp" #include "ExtrusionEntityCollection.hpp"
@@ -407,7 +407,7 @@ public:
double max_z() const { return m_max_z; } double max_z() const { return m_max_z; }
// Centering offset of the sliced mesh from the scaled and rotated mesh of the model. // Centering offset of the sliced mesh from the scaled and rotated mesh of the model.
const Point& center_offset() const { return m_center_offset; } const Point& center_offset() const { return m_center_offset; }
const TpmsRadialField* tpms_radial_field(TpmsAdaptiveMode mode) const { return m_tpms_radial_fields[size_t(mode)].get(); } const AdaptiveInfillField* adaptive_infill_field(AdaptiveInfillMode mode) const { return m_adaptive_infill_fields[size_t(mode)].get(); }
// BBS // BBS
void generate_support_preview(); void generate_support_preview();
@@ -655,7 +655,7 @@ private:
FillAdaptive::RegionOctrees prepare_adaptive_infill_data( FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const; const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data(); FillLightning::GeneratorPtr prepare_lightning_infill_data();
TpmsRadialFields prepare_tpms_radial_fields() const; AdaptiveInfillFields prepare_adaptive_infill_fields() const;
// BBS // BBS
SupportNecessaryType is_support_necessary(); SupportNecessaryType is_support_necessary();
@@ -706,7 +706,7 @@ private:
FillAdaptive::RegionOctrees m_adaptive_fill_octrees; FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes; std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator; FillLightning::GeneratorPtr m_lightning_generator;
TpmsRadialFields m_tpms_radial_fields; AdaptiveInfillFields m_adaptive_infill_fields;
std::vector < VolumeSlices > firstLayerObjSliceByVolume; std::vector < VolumeSlices > firstLayerObjSliceByVolume;
std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups; std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups;
+111 -28
View File
@@ -366,24 +366,24 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder)
//Orca //Orca
static t_config_enum_values s_keys_map_TpmsAdaptiveMode{ static t_config_enum_values s_keys_map_AdaptiveInfillMode{
{ "disabled", int(TpmsAdaptiveMode::Disabled) }, { "disabled", int(AdaptiveInfillMode::Disabled) },
{ "distance_warp", int(TpmsAdaptiveMode::DistanceWarp) }, { "distance_warp", int(AdaptiveInfillMode::DistanceWarp) },
{ "smooth_blend", int(TpmsAdaptiveMode::SmoothBlend) }, { "smooth_blend", int(AdaptiveInfillMode::SmoothBlend) },
{ "stepped_shells", int(TpmsAdaptiveMode::SteppedShells) }, { "stepped_shells", int(AdaptiveInfillMode::SteppedShells) },
{ "lobes", int(TpmsAdaptiveMode::Lobes) }, { "lobes", int(AdaptiveInfillMode::Lobes) },
{ "normal_z", int(TpmsAdaptiveMode::NormalZ) }, { "normal_z", int(AdaptiveInfillMode::NormalZ) },
{ "normal_y", int(TpmsAdaptiveMode::NormalY) }, { "normal_y", int(AdaptiveInfillMode::NormalY) },
{ "normal_x", int(TpmsAdaptiveMode::NormalX) }, { "normal_x", int(AdaptiveInfillMode::NormalX) },
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillMode)
static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{ static t_config_enum_values s_keys_map_AdaptiveInfillGradient{
{ "linear", int(TpmsAdaptiveGradient::Linear) }, { "linear", int(AdaptiveInfillGradient::Linear) },
{ "quadratic", int(TpmsAdaptiveGradient::Quadratic) }, { "quadratic", int(AdaptiveInfillGradient::Quadratic) },
{ "exponential", int(TpmsAdaptiveGradient::Exponential) }, { "exponential", int(AdaptiveInfillGradient::Exponential) },
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillGradient)
//BBS //BBS
static t_config_enum_values s_keys_map_PrintSequence { static t_config_enum_values s_keys_map_PrintSequence {
@@ -3660,7 +3660,7 @@ void PrintConfigDef::init_fff_params()
def->max = 10; // Maximum number of lines for infill pattern def->max = 10; // Maximum number of lines for infill pattern
def->set_default_value(new ConfigOptionInt(1)); def->set_default_value(new ConfigOptionInt(1));
def = this->add("tpms_adaptive", coEnum); def = this->add("adaptive_infill_mode", coEnum);
def->label = L("Adaptive density (experimental)"); def->label = L("Adaptive density (experimental)");
def->category = L("Strength"); def->category = L("Strength");
def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the " def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the "
@@ -3678,7 +3678,7 @@ void PrintConfigDef::init_fff_params()
"joined to the rest by a narrower neck, is graded towards its own center.\n" "joined to the rest by a narrower neck, is graded towards its own center.\n"
" - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density " " - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density "
"does not change along it."); "does not change along it.");
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveMode>::get_enum_values(); def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillMode>::get_enum_values();
def->enum_values.push_back("disabled"); def->enum_values.push_back("disabled");
def->enum_values.push_back("distance_warp"); def->enum_values.push_back("distance_warp");
def->enum_values.push_back("smooth_blend"); def->enum_values.push_back("smooth_blend");
@@ -3696,9 +3696,9 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Normal Y")); def->enum_labels.push_back(L("Normal Y"));
def->enum_labels.push_back(L("Normal X")); def->enum_labels.push_back(L("Normal X"));
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveMode>(TpmsAdaptiveMode::Disabled)); def->set_default_value(new ConfigOptionEnum<AdaptiveInfillMode>(AdaptiveInfillMode::Disabled));
def = this->add("tpms_interior_density", coPercent); def = this->add("adaptive_infill_interior_density", coPercent);
def->label = L("Interior density"); def->label = L("Interior density");
def->category = L("Strength"); def->category = L("Strength");
def->tooltip = L("Density of the adaptive infill at the center of the object."); def->tooltip = L("Density of the adaptive infill at the center of the object.");
@@ -3708,7 +3708,7 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionPercent(5)); def->set_default_value(new ConfigOptionPercent(5));
def = this->add("tpms_adaptive_gradient", coEnum); def = this->add("adaptive_infill_gradient", coEnum);
def->label = L("Adaptive gradient"); def->label = L("Adaptive gradient");
def->category = L("Strength"); def->category = L("Strength");
def->tooltip = L("How the density changes from the surface to the center of the object.\n" def->tooltip = L("How the density changes from the surface to the center of the object.\n"
@@ -3717,7 +3717,7 @@ void PrintConfigDef::init_fff_params()
"changes faster towards the center.\n" "changes faster towards the center.\n"
"Exponential: the density changes quickly just below the surface and levels off towards " "Exponential: the density changes quickly just below the surface and levels off towards "
"the center."); "the center.");
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveGradient>::get_enum_values(); def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillGradient>::get_enum_values();
def->enum_values.push_back("linear"); def->enum_values.push_back("linear");
def->enum_values.push_back("quadratic"); def->enum_values.push_back("quadratic");
def->enum_values.push_back("exponential"); def->enum_values.push_back("exponential");
@@ -3725,7 +3725,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Quadratic")); def->enum_labels.push_back(L("Quadratic"));
def->enum_labels.push_back(L("Exponential")); def->enum_labels.push_back(L("Exponential"));
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveGradient>(TpmsAdaptiveGradient::Linear)); def->set_default_value(new ConfigOptionEnum<AdaptiveInfillGradient>(AdaptiveInfillGradient::Linear));
// Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z // Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z
// (vertical) axis at low infill density to shorten the effective column length under // (vertical) axis at low infill density to shorten the effective column length under
@@ -9665,6 +9665,12 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key = "change_filament_gcode"; opt_key = "change_filament_gcode";
} else if (opt_key == "bridge_fan_speed") { } else if (opt_key == "bridge_fan_speed") {
opt_key = "overhang_fan_speed"; opt_key = "overhang_fan_speed";
} else if (opt_key == "tpms_adaptive") {
opt_key = "adaptive_infill_mode";
} else if (opt_key == "tpms_interior_density") {
opt_key = "adaptive_infill_interior_density";
} else if (opt_key == "tpms_adaptive_gradient") {
opt_key = "adaptive_infill_gradient";
} else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") { } else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") {
// ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the // ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the
// default value "1" meant "the first/active filament". The current scheme uses a dedicated // default value "1" meant "the first/active filament". The current scheme uses a dedicated
@@ -13487,12 +13493,30 @@ static std::map<t_custom_gcode_key, t_config_option_keys> s_CustomGcodeSpecificP
// Machine G-code // Machine G-code
{"file_start_gcode", {}}, {"file_start_gcode", {}},
{"machine_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"}}, {"before_layer_change_gcode", {"layer_num", "layer_z", "max_layer_z"}},
{"layer_change_gcode", {"layer_num", "layer_z", "max_layer_z"}}, {"layer_change_gcode", {"layer_num", "layer_z", "max_layer_z", "most_used_physical_extruder_id",
{"timelapse_gcode", {"layer_num", "layer_z", "max_layer_z"}}, "current_filament_id", "current_nozzle_id", "curr_y_acceleration_limit",
{"change_filament_gcode", {"layer_num", "layer_z", "max_layer_z", "next_extruder", "previous_extruder", "fan_speed", "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", "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_filament_e_feedrate", "new_filament_temp", "new_retract_length",
"new_retract_length_toolchange", "old_filament_e_feedrate", "old_filament_temp", "old_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", "old_retract_length_toolchange", "relative_e_axis", "second_flush_volume", "toolchange_count", "toolchange_z",
@@ -13505,8 +13529,18 @@ static std::map<t_custom_gcode_key, t_config_option_keys> s_CustomGcodeSpecificP
{"machine_pause_gcode", {}}, {"machine_pause_gcode", {}},
{"template_custom_gcode", {}}, {"template_custom_gcode", {}},
// Filament G-code // Filament G-code
{"filament_start_gcode", {"filament_extruder_id"}}, {"filament_start_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
{"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"}},
{"filament_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
"current_filament_id", "current_extruder_id", "current_nozzle_id",
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types",
"retraction_distance_when_cut", "long_retraction_when_cut",
"temperature", "nozzle_temperature", "first_layer_temperature",
"nozzle_temperature_initial_layer"}},
}; };
const std::map<t_custom_gcode_key, t_config_option_keys>& custom_gcode_specific_placeholders() const std::map<t_custom_gcode_key, t_config_option_keys>& custom_gcode_specific_placeholders()
@@ -13535,11 +13569,44 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
def->label = L("Filament extruder ID"); def->label = L("Filament extruder ID");
def->tooltip = L("The current extruder ID. The same as current_extruder."); 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 // 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("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("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("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_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("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("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.")); new_def("new_retract_length", coFloat, L("New retract length"), L("The retraction length of the new filament."));
@@ -13564,6 +13631,22 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
new_def("flush_length_2", coFloat, L("Flush Length 2"), L("The second flush length.")); 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_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_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 // change_extrusion_role_gcode
// The whole tooltip is one literal so that the GUI can look it up in the catalog. // The whole tooltip is one literal so that the GUI can look it up in the catalog.
+11 -11
View File
@@ -172,8 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
// Orca: Infill patterns laid out by an octree, which each connected body always gets of its own. // Orca: Infill patterns laid out by an octree, which each connected body always gets of its own.
inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; } inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; }
// Orca: Infill patterns graded by the "tpms_adaptive" option. // Orca: Infill patterns graded by the "adaptive_infill_mode" option.
inline bool is_tpms_adaptive_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; } inline bool is_adaptive_infill_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option. // Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more // Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
@@ -279,8 +279,8 @@ enum class SurfaceFillOrder {
Count, Count,
}; };
// Orca: what the adaptive TPMS density follows: the 3D shape of the object, or its 2D sections normal to an axis. // Orca: what the adaptive infill density follows: the 3D shape of the object, or its 2D sections normal to an axis.
enum class TpmsAdaptiveMode { enum class AdaptiveInfillMode {
Disabled, Disabled,
DistanceWarp, DistanceWarp,
SmoothBlend, SmoothBlend,
@@ -292,8 +292,8 @@ enum class TpmsAdaptiveMode {
Count, Count,
}; };
// Orca: how the adaptive TPMS density changes from the object surface to its deepest point. // Orca: how the adaptive infill density changes from the object surface to its deepest point.
enum class TpmsAdaptiveGradient { enum class AdaptiveInfillGradient {
Linear, Linear,
Quadratic, Quadratic,
Exponential, Exponential,
@@ -816,8 +816,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveGradient) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillGradient)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -1501,9 +1501,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca: // Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height)) ((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline)) ((ConfigOptionInt, fill_multiline))
((ConfigOptionEnum<TpmsAdaptiveMode>, tpms_adaptive)) ((ConfigOptionEnum<AdaptiveInfillMode>, adaptive_infill_mode))
((ConfigOptionPercent, tpms_interior_density)) ((ConfigOptionPercent, adaptive_infill_interior_density))
((ConfigOptionEnum<TpmsAdaptiveGradient>, tpms_adaptive_gradient)) ((ConfigOptionEnum<AdaptiveInfillGradient>, adaptive_infill_gradient))
((ConfigOptionBool, gyroid_optimized)) ((ConfigOptionBool, gyroid_optimized))
// Ironing options // Ironing options
((ConfigOptionEnum<IroningType>, ironing_type)) ((ConfigOptionEnum<IroningType>, ironing_type))
+13 -13
View File
@@ -33,7 +33,7 @@
#include "Fill/FillAdaptive.hpp" #include "Fill/FillAdaptive.hpp"
#include "Fill/Fill.hpp" #include "Fill/Fill.hpp"
#include "Fill/FillLightning.hpp" #include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp" #include "Fill/AdaptiveInfillField.hpp"
#include "format.hpp" #include "format.hpp"
#include "AABBTreeIndirect.hpp" #include "AABBTreeIndirect.hpp"
#include "AABBTreeLines.hpp" #include "AABBTreeLines.hpp"
@@ -1252,19 +1252,19 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr(); return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr();
} }
TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const AdaptiveInfillFields PrintObject::prepare_adaptive_infill_fields() const
{ {
TpmsRadialFields fields; AdaptiveInfillFields fields;
std::array<bool, size_t(TpmsAdaptiveMode::Count)> modes{}; std::array<bool, size_t(AdaptiveInfillMode::Count)> modes{};
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id)
if (const PrintRegionConfig &config = this->printing_region(region_id).config(); if (const PrintRegionConfig &config = this->printing_region(region_id).config();
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_tpms_adaptive_pattern(config.sparse_infill_pattern)) config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_adaptive_infill_pattern(config.sparse_infill_pattern))
modes[size_t(config.tpms_adaptive.value)] = true; modes[size_t(config.adaptive_infill_mode.value)] = true;
modes[size_t(TpmsAdaptiveMode::Disabled)] = false; modes[size_t(AdaptiveInfillMode::Disabled)] = false;
if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty()) if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty())
return fields; return fields;
std::vector<TpmsRadialField::Slice> slices; std::vector<AdaptiveInfillField::Slice> slices;
slices.reserve(m_layers.size()); slices.reserve(m_layers.size());
BoundingBox bbox; BoundingBox bbox;
for (const Layer *layer : m_layers) { for (const Layer *layer : m_layers) {
@@ -1277,7 +1277,7 @@ TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
if (!modes[mode]) if (!modes[mode])
continue; continue;
// Without a field, the infill falls back to the regular pattern. // Without a field, the infill falls back to the regular pattern.
auto field = std::make_unique<TpmsRadialField>(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); }); auto field = std::make_unique<AdaptiveInfillField>(slices, bbox, AdaptiveInfillMode(mode), [this]() { m_print->throw_if_canceled(); });
if (!field->empty()) if (!field->empty())
fields[mode] = std::move(field); fields[mode] = std::move(field);
} }
@@ -1685,9 +1685,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_overhang_angle") { || opt_key == "infill_overhang_angle") {
steps.emplace_back(posInfill); steps.emplace_back(posInfill);
} else if (opt_key == "sparse_infill_pattern" } else if (opt_key == "sparse_infill_pattern"
|| opt_key == "tpms_adaptive" || opt_key == "adaptive_infill_mode"
|| opt_key == "tpms_interior_density" || opt_key == "adaptive_infill_interior_density"
|| opt_key == "tpms_adaptive_gradient" || opt_key == "adaptive_infill_gradient"
// Orca: Body centering now also determines bridge anchors during preparation. // Orca: Body centering now also determines bridge anchors during preparation.
// Invalidating preparation also invalidates infill, including top/bottom surfaces. // Invalidating preparation also invalidates infill, including top/bottom surfaces.
|| opt_key == "center_of_surface_pattern" || opt_key == "center_of_surface_pattern"
@@ -3195,7 +3195,7 @@ void PrintObject::bridge_over_infill()
} }
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer); this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
this->m_tpms_radial_fields = this->prepare_tpms_radial_fields(); this->m_adaptive_infill_fields = this->prepare_adaptive_infill_fields();
std::vector<size_t> layers_to_generate_infill; std::vector<size_t> layers_to_generate_infill;
for (const auto &pair : surfaces_by_layer) { for (const auto &pair : surfaces_by_layer) {
+484 -11
View File
@@ -26,8 +26,10 @@
#include "Print.hpp" #include "Print.hpp"
#include "GCode/OrderingStrategies.hpp" #include "GCode/OrderingStrategies.hpp"
#include <array>
#include <cmath> #include <cmath>
#include <cassert> #include <cassert>
#include <deque>
#include "MultiMaterialSegmentation.hpp" #include "MultiMaterialSegmentation.hpp"
namespace Slic3r { namespace Slic3r {
@@ -96,6 +98,46 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
return out; return out;
} }
// The shortest chain over every order and allowed reversal, by dynamic programming over the subsets of segments, so only
// for a few of them. end_points holds the first and the last point of each segment; the chain starts at start when given
// and its end is free. Return index and "reversed" flag.
static std::vector<std::pair<size_t, bool>> chain_segments_exact(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d *start)
{
const size_t n = could_reverse.size();
assert(n >= 1 && n <= 12 && end_points.size() == 2 * n);
// State (mask * n + j) * 2 + o: the segments of mask visited, the last one j, flipped when o.
const size_t num_states = (size_t(1) << n) * n * 2;
std::vector<double> cost(num_states, std::numeric_limits<double>::max());
std::vector<size_t> from(num_states, size_t(-1));
auto entry = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + o]; };
auto exit = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + 1 - o]; };
for (size_t j = 0; j < n; ++ j)
for (size_t o = 0; o <= size_t(could_reverse[j] != 0); ++ o)
cost[((size_t(1) << j) * n + j) * 2 + o] = start == nullptr ? 0. : (entry(j, o) - *start).norm();
for (size_t mask = 1; mask < (size_t(1) << n); ++ mask)
for (size_t s = mask * n * 2; s < (mask + 1) * n * 2; ++ s)
if (cost[s] < std::numeric_limits<double>::max())
for (size_t k = 0; k < n; ++ k)
if (! (mask >> k & 1))
for (size_t o = 0; o <= size_t(could_reverse[k] != 0); ++ o) {
const size_t next = (((mask | size_t(1) << k) * n) + k) * 2 + o;
const double c = cost[s] + (entry(k, o) - exit(s / 2 % n, s & 1)).norm();
if (c < cost[next]) {
cost[next] = c;
from[next] = s;
}
}
const size_t full = (size_t(1) << n) - 1;
size_t best = full * n * 2;
for (size_t s = best; s < num_states; ++ s)
if (cost[s] < cost[best])
best = s;
std::vector<std::pair<size_t, bool>> out(n);
for (size_t s = best, k = n; k -- > 0; s = from[s])
out[k] = { s / 2 % n, (s & 1) != 0 };
return out;
}
// Chain perimeters (always closed) and thin fills (closed or open) using a greedy algorithm. // 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 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. // Solving using a greedy algorithm, where a shortest edge is added to the solution if it does not produce a bifurcation or a cycle.
@@ -104,6 +146,7 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
// The algorithm builds a tour for the traveling salesman one edge at a time and thus maintains multiple tour fragments, each of which // The algorithm builds a tour for the traveling salesman one edge at a time and thus maintains multiple tour fragments, each of which
// is a simple path in the complete graph of cities. At each stage, the algorithm selects the edge of minimal cost that either creates // is a simple path in the complete graph of cities. At each stage, the algorithm selects the edge of minimal cost that either creates
// a new fragment, extends one of the existing paths or creates a cycle of length equal to the number of cities. // a new fragment, extends one of the existing paths or creates a cycle of length equal to the number of cities.
// Orca: two to four segments are chained exactly instead.
template<typename PointType, typename SegmentEndPointFunc, bool REVERSE_COULD_FAIL, typename CouldReverseFunc> template<typename PointType, typename SegmentEndPointFunc, bool REVERSE_COULD_FAIL, typename CouldReverseFunc>
std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals_(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near) std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals_(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near)
{ {
@@ -118,6 +161,19 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals
out.emplace_back(0, could_reverse_func(0) && start_near != nullptr && out.emplace_back(0, could_reverse_func(0) && start_near != nullptr &&
(end_point_func(0, false) - *start_near).template cast<double>().squaredNorm() < (end_point_func(0, true) - *start_near).template cast<double>().squaredNorm()); (end_point_func(0, false) - *start_near).template cast<double>().squaredNorm() < (end_point_func(0, true) - *start_near).template cast<double>().squaredNorm());
} }
else if (num_segments <= 4)
{
// Orca: so few segments are cheap to chain exactly. The first one is then not always the one nearest start_near.
std::vector<Vec2d> end_points(2 * num_segments);
std::vector<char> could_reverse(num_segments);
for (size_t i = 0; i < num_segments; ++ i) {
end_points[2 * i] = end_point_func(i, true).template cast<double>();
end_points[2 * i + 1] = end_point_func(i, false).template cast<double>();
could_reverse[i] = could_reverse_func(i);
}
const Vec2d start = start_near != nullptr ? Vec2d(start_near->template cast<double>()) : Vec2d::Zero();
out = chain_segments_exact(end_points, could_reverse, start_near != nullptr ? &start : nullptr);
}
else else
{ {
// End points of segments for the KD tree closest point search. // End points of segments for the KD tree closest point search.
@@ -1039,12 +1095,370 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy2(SegmentEndPointFunc
return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near); return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near);
} }
// The K points nearest to a query, nearest first, ties by index, from a uniform grid of about two points per cell.
template<size_t K>
class NearestPoints
{
public:
static constexpr size_t npos = size_t(-1);
explicit NearestPoints(const std::vector<Vec2d> &points) : m_points(points)
{
BoundingBoxf bbox;
for (const Vec2d &p : points)
bbox.merge(p);
const Vec2d size = bbox.size();
const double count = double(std::max<size_t>(points.size(), 1));
m_cell = std::max(std::sqrt(2. * size.x() * size.y() / count), std::max(size.x(), size.y()) / count);
if (m_cell <= 0.)
m_cell = 1.;
m_origin = bbox.min;
m_cols = int(size.x() / m_cell) + 1;
m_rows = int(size.y() / m_cell) + 1;
m_first.assign(size_t(m_cols) * m_rows + 1, 0);
for (const Vec2d &p : points)
++ m_first[cell(p) + 1];
for (size_t c = 1; c < m_first.size(); ++ c)
m_first[c] += m_first[c - 1];
m_index.resize(points.size());
std::vector<size_t> next(m_first.begin(), m_first.end() - 1);
for (size_t i = 0; i < points.size(); ++ i)
m_index[next[cell(points[i])] ++] = i;
}
// Indices of the nearest points that filter accepts, npos past the last one.
template<typename Filter>
std::array<size_t, K> query(const Vec2d &q, Filter filter) const
{
std::array<std::pair<double, size_t>, K> best;
size_t found = 0;
const int cx = int(std::clamp(std::floor((q.x() - m_origin.x()) / m_cell), 0., double(m_cols - 1)));
const int cy = int(std::clamp(std::floor((q.y() - m_origin.y()) / m_cell), 0., double(m_rows - 1)));
auto visit = [&](int x, int y) {
if (x < 0 || y < 0 || x >= m_cols || y >= m_rows)
return;
const size_t c = size_t(y) * m_cols + x;
for (size_t k = m_first[c]; k < m_first[c + 1]; ++ k) {
const size_t i = m_index[k];
if (! filter(i))
continue;
const std::pair<double, size_t> candidate((m_points[i] - q).squaredNorm(), i);
if (found == K && ! (candidate < best[K - 1]))
continue;
auto it = std::upper_bound(best.begin(), best.begin() + found, candidate);
std::move_backward(it, best.begin() + std::min(found, K - 1), best.begin() + std::min(found + 1, K));
*it = candidate;
found = std::min(found + 1, K);
}
};
// Points beyond ring r are at least r cells away.
for (int r = 0; r <= std::max(m_cols, m_rows); ++ r) {
if (r == 0)
visit(cx, cy);
for (int d = -r; d <= r && r > 0; ++ d) {
visit(cx + d, cy - r);
visit(cx + d, cy + r);
if (d != -r && d != r) {
visit(cx - r, cy + d);
visit(cx + r, cy + d);
}
}
if (found == K && best[K - 1].first <= sqr(r * m_cell))
break;
}
std::array<size_t, K> out;
out.fill(npos);
for (size_t k = 0; k < found; ++ k)
out[k] = best[k].second;
return out;
}
private:
size_t cell(const Vec2d &p) const
{
const int x = std::clamp(int((p.x() - m_origin.x()) / m_cell), 0, m_cols - 1);
const int y = std::clamp(int((p.y() - m_origin.y()) / m_cell), 0, m_rows - 1);
return size_t(y) * m_cols + x;
}
const std::vector<Vec2d> &m_points;
Vec2d m_origin;
double m_cell;
int m_cols;
int m_rows;
std::vector<size_t> m_first;
std::vector<size_t> m_index;
};
// Shortens the travel of a chain with 2-opt moves (reversing a run of segments) and or-opt moves (moving a run of up to
// three segments elsewhere, or a longer one to the front, flipped or not). end_points holds the first and the last point
// of each segment. The chain starts at start_near when given, and its end is free. A move that would flip a segment that
// cannot reverse is skipped. Moves only create links between nearest neighbor end points, and after a move only the
// segments next to a changed link are examined again, until a sweep of all segments finds no move. This keeps the search
// close to linear.
static void improve_chain_by_local_search(std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points,
const std::vector<char> &could_reverse, const Vec2d *start_near)
{
const size_t n = chain.size();
if (n < 2)
return;
assert(end_points.size() == 2 * n && could_reverse.size() == n);
std::vector<size_t> order(n), pos(n), fixed_before(n + 1, 0);
std::vector<char> flipped(n);
for (size_t k = 0; k < n; ++ k) {
order[k] = chain[k].first;
flipped[order[k]] = chain[k].second;
}
// Updates positions lo..hi after a move, which keeps the number of segments that cannot reverse past hi.
auto refresh = [&](size_t lo, size_t hi) {
for (size_t k = lo; k <= hi; ++ k) {
pos[order[k]] = k;
fixed_before[k + 1] = fixed_before[k] + ! could_reverse[order[k]];
}
};
refresh(0, n - 1);
auto entry_idx = [&](size_t k) { return 2 * order[k] + flipped[order[k]]; };
auto exit_idx = [&](size_t k) { return 2 * order[k] + 1 - flipped[order[k]]; };
auto entry = [&](size_t k) -> const Vec2d& { return end_points[entry_idx(k)]; };
auto exit = [&](size_t k) -> const Vec2d& { return end_points[exit_idx(k)]; };
auto is_exit = [&](size_t e) { return char(e & 1) != flipped[e >> 1]; };
auto reversible = [&](size_t i, size_t j) { return fixed_before[j + 1] == fixed_before[i]; };
auto dist = [](const Vec2d *a, const Vec2d *b) { return a == nullptr || b == nullptr ? 0. : (*a - *b).norm(); };
// Travel end points around position k, null where the chain is open.
auto prev_exit = [&](size_t k) -> const Vec2d* { return k > 0 ? &exit(k - 1) : start_near; };
auto next_entry = [&](size_t k) -> const Vec2d* { return k + 1 < n ? &entry(k + 1) : nullptr; };
// The 32 nearest end points are the candidates, which covers every move on short chains; 8 missed much of the gain.
static constexpr size_t K = 32;
static constexpr size_t npos = NearestPoints<K>::npos;
const NearestPoints<K> nearest(end_points);
std::vector<std::array<size_t, K>> neighbors(end_points.size());
for (size_t e = 0; e < end_points.size(); ++ e)
neighbors[e] = nearest.query(end_points[e], [e](size_t idx) { return (idx >> 1) != (e >> 1); });
std::array<size_t, K> start_neighbors;
start_neighbors.fill(npos);
if (start_near != nullptr)
start_neighbors = nearest.query(*start_near, [](size_t) { return true; });
// Segments to examine, by id, each queued once.
std::deque<size_t> queue;
std::vector<char> queued(n, 0);
auto enqueue = [&](size_t k) {
if (k < n && ! queued[order[k]]) {
queued[order[k]] = 1;
queue.push_back(order[k]);
}
};
const double min_gain = SCALED_EPSILON;
// Reverses positions i..j if that shortens the chain.
auto try_2opt = [&](size_t i, size_t j) {
if (! reversible(i, j))
return false;
const Vec2d *a = prev_exit(i), *b = next_entry(j);
if (dist(a, &exit(j)) + dist(&entry(i), b) > dist(a, &entry(i)) + dist(&exit(j), b) - min_gain)
return false;
std::reverse(order.begin() + i, order.begin() + j + 1);
for (size_t k = i; k <= j; ++ k)
flipped[order[k]] ^= 1;
refresh(i, j);
for (size_t k : { i - 1, i, j, j + 1 })
enqueue(k);
return true;
};
// Moves positions i..last after position gap (size_t(-1) for the front) if that shortens the chain.
auto try_oropt = [&](size_t i, size_t last, size_t gap) {
if (gap + 1 == i || (gap >= i && gap <= last))
return false;
const Vec2d *a = gap == size_t(-1) ? start_near : &exit(gap);
const Vec2d *b = gap + 1 < n ? &entry(gap + 1) : nullptr;
const double removal_gain = dist(prev_exit(i), &entry(i)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(i), next_entry(last));
const double base = dist(a, b);
const double cost_kept = dist(a, &entry(i)) + dist(&exit(last), b) - base;
const double cost_flipped = reversible(i, last) ? dist(a, &exit(last)) + dist(&entry(i), b) - base : std::numeric_limits<double>::max();
if (std::min(cost_kept, cost_flipped) > removal_gain - min_gain)
return false;
const size_t len = last - i + 1;
// The run lands on first..first+len-1; lo..hi spans every position that changed.
const bool forward = gap > last && gap != size_t(-1);
const size_t first = forward ? gap + 1 - len : gap + 1;
const size_t lo = forward ? i : first;
const size_t hi = forward ? gap : last;
if (forward)
std::rotate(order.begin() + i, order.begin() + last + 1, order.begin() + gap + 1);
else
std::rotate(order.begin() + first, order.begin() + i, order.begin() + last + 1);
if (cost_flipped < cost_kept) {
std::reverse(order.begin() + first, order.begin() + first + len);
for (size_t k = first; k < first + len; ++ k)
flipped[order[k]] ^= 1;
}
refresh(lo, hi);
// The links around the run's new place, and the link closing its old place.
for (size_t k : { first - 1, first, first + len - 1, first + len, forward ? i - 1 : last, forward ? i : last + 1 })
enqueue(k);
return true;
};
// The gap before position k of the chain with positions i..last taken out.
auto gap_before = [](size_t k, size_t i, size_t last) { return k == last + 1 ? i - 1 : k - 1; };
// Applies the first move found that breaks a link next to position p.
// Candidates are tried nearest first, while closer than the link the move would replace at that end point.
auto closer = [&](const Vec2d &at, size_t q, double limit) { return q != npos && (end_points[q] - at).norm() < limit; };
auto improve_at = [&](size_t p) {
// 2-opt breaking the link into p, as the first or the last link of the reversed run.
if (const Vec2d *a = prev_exit(p); a != nullptr) {
const double old_link = dist(a, &entry(p));
for (size_t q : p > 0 ? neighbors[exit_idx(p - 1)] : start_neighbors) {
if (! closer(*a, q, old_link))
break;
if (is_exit(q) && pos[q >> 1] >= p && try_2opt(p, pos[q >> 1]))
return true;
}
if (p > 0)
for (size_t q : neighbors[entry_idx(p)]) {
if (! closer(entry(p), q, old_link))
break;
if (! is_exit(q) && pos[q >> 1] < p && try_2opt(pos[q >> 1], p - 1))
return true;
}
}
// 2-opt breaking the link out of p.
if (p + 1 < n) {
const double old_link = dist(&exit(p), &entry(p + 1));
for (size_t q : neighbors[exit_idx(p)]) {
if (! closer(exit(p), q, old_link))
break;
if (is_exit(q) && pos[q >> 1] > p && try_2opt(p + 1, pos[q >> 1]))
return true;
}
for (size_t q : neighbors[entry_idx(p + 1)]) {
if (! closer(entry(p + 1), q, old_link))
break;
if (! is_exit(q) && pos[q >> 1] <= p && try_2opt(pos[q >> 1], p))
return true;
}
}
// A run of any length that starts or ends next to the start point moves to the front.
if (p == 0)
for (size_t q : start_neighbors) {
if (q == npos)
break;
const size_t m = pos[q >> 1];
if (m == 0)
continue;
// Runs m..k enter next to the start, runs k..m leave there and go flipped.
const bool exit_near = is_exit(q);
for (size_t k = exit_near ? 1 : m; k < (exit_near ? m + 1 : n); ++ k)
if (try_oropt(exit_near ? k : m, exit_near ? m : k, size_t(-1)))
return true;
}
// Or-opt of the runs starting at p: next to an exit a run goes after that segment, next to an entry before it.
for (size_t last = p; last < std::min(p + 3, n); ++ last) {
if (try_oropt(p, last, size_t(-1)))
return true;
const double removal_gain = dist(prev_exit(p), &entry(p)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(p), next_entry(last));
for (size_t e : { entry_idx(p), exit_idx(last) })
for (size_t q : neighbors[e]) {
if (! closer(end_points[e], q, removal_gain))
break;
const size_t m = pos[q >> 1];
if ((m < p || m > last) && try_oropt(p, last, is_exit(q) ? m : gap_before(m, p, last)))
return true;
}
}
return false;
};
// A new link can make room for a run that sits elsewhere, hence the sweeps until one finds no move. Every move shortens
// the chain, so the search ends; the cap only bounds its time.
for (size_t k = 0; k < n; ++ k)
enqueue(k);
bool moved_since_sweep = false;
for (size_t moves = 0; moves < 50 * n;) {
if (queue.empty()) {
if (! moved_since_sweep)
break;
moved_since_sweep = false;
for (size_t k = 0; k < n; ++ k)
enqueue(k);
}
const size_t segment = queue.front();
queue.pop_front();
queued[segment] = 0;
if (improve_at(pos[segment])) {
++ moves;
moved_since_sweep = true;
enqueue(pos[segment]);
}
}
for (size_t k = 0; k < n; ++ k)
chain[k] = { order[k], flipped[order[k]] != 0 };
}
// Chain that always goes on to the nearest free end point, from start.
static std::vector<std::pair<size_t, bool>> chain_nearest_next(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d &start)
{
const NearestPoints<1> nearest(end_points);
std::vector<char> taken(could_reverse.size(), 0);
std::vector<std::pair<size_t, bool>> out;
out.reserve(could_reverse.size());
for (Vec2d at = start; out.size() < could_reverse.size();) {
const size_t e = nearest.query(at, [&](size_t idx) { return ! taken[idx >> 1] && ((idx & 1) == 0 || could_reverse[idx >> 1]); })[0];
taken[e >> 1] = 1;
out.emplace_back(e >> 1, (e & 1) != 0);
at = end_points[e ^ 1];
}
return out;
}
static double chain_travel(const std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points, const Vec2d &start)
{
double travel = 0.;
Vec2d at = start;
for (const std::pair<size_t, bool> &segment : chain) {
travel += (end_points[2 * segment.first + segment.second] - at).norm();
at = end_points[2 * segment.first + 1 - segment.second];
}
return travel;
}
template<typename EntityPtr> template<typename EntityPtr>
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near) static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near, bool improve = false)
{ {
auto segment_end_point = [&entities](size_t idx, bool first_point) -> Point { return first_point ? entities[idx]->first_point() : entities[idx]->last_point(); }; auto 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(); }; auto could_reverse = [&entities](size_t idx) { const ExtrusionEntity *ee = entities[idx]; return ee->is_loop() || ee->can_reverse(); };
std::vector<std::pair<size_t, bool>> out = chain_segments_greedy_constrained_reversals<Point, decltype(segment_end_point), decltype(could_reverse)>(segment_end_point, could_reverse, entities.size(), start_near); std::vector<std::pair<size_t, bool>> out = chain_segments_greedy_constrained_reversals<Point, decltype(segment_end_point), decltype(could_reverse)>(segment_end_point, could_reverse, entities.size(), start_near);
if (improve && out.size() > 1) {
std::vector<Vec2d> end_points;
std::vector<char> reversible;
end_points.reserve(2 * entities.size());
reversible.reserve(entities.size());
for (size_t idx = 0; idx < entities.size(); ++ idx) {
end_points.emplace_back(segment_end_point(idx, true).template cast<double>());
end_points.emplace_back(segment_end_point(idx, false).template cast<double>());
reversible.emplace_back(could_reverse(idx));
}
Vec2d start = Vec2d::Zero();
if (start_near != nullptr)
start = start_near->cast<double>();
if (entities.size() <= 8) {
// Few paths are chained exactly; the local search below can stop short of the shortest chain.
out = chain_segments_exact(end_points, reversible, start_near != nullptr ? &start : nullptr);
} else {
improve_chain_by_local_search(out, end_points, reversible, start_near != nullptr ? &start : nullptr);
if (start_near != nullptr) {
// The greedy chain can settle into a sweep that local moves cannot undo, which a nearest-next chain may avoid.
std::vector<std::pair<size_t, bool>> nearest_next = chain_nearest_next(end_points, reversible, start);
if (chain_travel(nearest_next, end_points, start) < chain_travel(out, end_points, start) - SCALED_EPSILON) {
improve_chain_by_local_search(nearest_next, end_points, reversible, &start);
out = std::move(nearest_next);
}
}
}
}
for (std::pair<size_t, bool> &segment : out) { for (std::pair<size_t, bool> &segment : out) {
const ExtrusionEntity *ee = entities[segment.first]; const ExtrusionEntity *ee = entities[segment.first];
if (ee->is_loop()) if (ee->is_loop())
@@ -1086,7 +1500,8 @@ void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const s
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near) void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near)
{ {
chain_and_reorder_extrusion_entities(entities, &start_near); remove_entities_without_endpoints(entities);
reorder_extrusion_entities(entities, chain_extrusion_entities_impl(entities, &start_near, true));
} }
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near) void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near)
@@ -1101,7 +1516,7 @@ void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &e
remove_entities_without_endpoints(entities); remove_entities_without_endpoints(entities);
std::vector<const ExtrusionEntity*> out; std::vector<const ExtrusionEntity*> out;
out.reserve(entities.size()); 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) { if (reverse) {
ExtrusionEntity *clone = reversed_clones.emplace_back(entities[idx]->clone()).get(); ExtrusionEntity *clone = reversed_clones.emplace_back(entities[idx]->clone()).get();
clone->reverse(); clone->reverse();
@@ -1159,16 +1574,74 @@ std::vector<size_t> chain_points(const Points &points, const Point *start_near)
return out; return out;
} }
// Moves runs of up to three points of the cycle next to one of the 8 points nearest to an end of the run, reversed or not,
// while that shortens the cycle. path[0] stays first. Returns true if any move was made.
static bool improve_cycle_by_or_opt(std::vector<size_t> &path, const Points &points)
{
const size_t pn = path.size();
if (pn < 5)
return false;
std::vector<Vec2d> coords(points.size());
for (size_t i = 0; i < points.size(); ++ i)
coords[i] = points[i].cast<double>();
const NearestPoints<8> grid(coords);
std::vector<size_t> pos(points.size());
auto refresh = [&]() { for (size_t k = 0; k < pn; ++ k) pos[path[k]] = k; };
refresh();
auto dist = [&](size_t a, size_t b) { return (coords[path[a % pn]] - coords[path[b % pn]]).norm(); };
bool improved = false;
for (bool moved = true; moved;) {
moved = false;
for (size_t len = 1; len <= 3; ++ len)
for (size_t i = 1; i + len <= pn; ++ i) {
// The run path[i..last] goes between path[k] and path[k + 1].
const size_t last = i + len - 1;
const double removal = dist(i - 1, i) + dist(last, last + 1) - dist(i - 1, last + 1);
bool applied = removal <= SCALED_EPSILON;
for (size_t end : { path[i], path[last] })
for (size_t q : grid.query(coords[end], [end](size_t idx) { return idx != end; }))
for (size_t k : { q == NearestPoints<8>::npos ? pn : (pos[q] + pn - 1) % pn, q == NearestPoints<8>::npos ? pn : pos[q] }) {
if (applied || k == pn || (k + 1 >= i && k <= last))
continue;
const double base = dist(k, k + 1);
const double kept = dist(k, i) + dist(last, k + 1) - base;
const double reversed = dist(k, last) + dist(i, k + 1) - base;
if (std::min(kept, reversed) > removal - SCALED_EPSILON)
continue;
std::vector<size_t> run(path.begin() + i, path.begin() + last + 1);
if (reversed < kept)
std::reverse(run.begin(), run.end());
path.erase(path.begin() + i, path.begin() + last + 1);
path.insert(path.begin() + (k < i ? k + 1 : k + 1 - len), run.begin(), run.end());
refresh();
applied = moved = improved = true;
}
}
}
return improved;
}
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near) std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
{ {
std::vector<size_t> path = chain_points(points, start_near); std::vector<size_t> path;
// Alternate 2-opt and crossing removal until convergence. if (points.size() <= 10) {
// 2-opt can create new crossings, and crossing removal can create new // Orca: few points are toured exactly, from the point nearest start_near toward its nearer neighbor.
// opportunities for 2-opt improvement. Break early if neither improves. path = tsp_exact_cycle(points);
for (int iter = 0; iter < 3; ++iter) { if (start_near != nullptr && path.size() > 2) {
bool improved = tsp_2opt_improve(path, points); auto dist = [&points](size_t a, const Point &b) { return (points[a] - b).cast<double>().squaredNorm(); };
improved |= tsp_remove_crossings(path, points); 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 (!improved) break; 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) if (start_near == nullptr)
tsp_rotate_minimize_closing(path, points); tsp_rotate_minimize_closing(path, points);
+4 -1
View File
@@ -23,9 +23,12 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr); std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const std::vector<std::pair<size_t, bool>> &chain); void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const std::vector<std::pair<size_t, bool>> &chain);
// From start_near, the greedy chain is refined by 2-opt and or-opt moves, which shortens the travel at some extra cost.
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near); void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near);
// The greedy chain only.
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr); void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
// Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the originals stay unchanged. // Refined as above. Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the
// originals stay unchanged.
void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &entities, const Point &start_near, void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &entities, const Point &start_near,
std::vector<std::unique_ptr<ExtrusionEntity>> &reversed_clones); std::vector<std::unique_ptr<ExtrusionEntity>> &reversed_clones);
+8 -8
View File
@@ -843,17 +843,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric || bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral; pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
// The sparse infill density is the surface density of the adaptive TPMS infill; at 100% the infill is solid. // The sparse infill density is the surface density of the adaptive infill; at 100% the infill is solid.
bool have_tpms_infill = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 && bool have_adaptive_infill_pattern = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
is_tpms_adaptive_pattern(pattern); is_adaptive_infill_pattern(pattern);
toggle_line("tpms_adaptive", have_tpms_infill); toggle_line("adaptive_infill_mode", have_adaptive_infill_pattern);
bool have_tpms_adaptive = have_tpms_infill && config->opt_enum<TpmsAdaptiveMode>("tpms_adaptive") != TpmsAdaptiveMode::Disabled; bool have_adaptive_infill = have_adaptive_infill_pattern && config->opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") != AdaptiveInfillMode::Disabled;
toggle_line("tpms_interior_density", have_tpms_adaptive); toggle_line("adaptive_infill_interior_density", have_adaptive_infill);
toggle_line("tpms_adaptive_gradient", have_tpms_adaptive); toggle_line("adaptive_infill_gradient", have_adaptive_infill);
// gyroid_optimized only applies when the sparse infill pattern is gyroid without adaptive density; // gyroid_optimized only applies when the sparse infill pattern is gyroid without adaptive density;
// hide the whole line otherwise. // hide the whole line otherwise.
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_tpms_adaptive); toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_adaptive_infill);
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill. // If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
if (have_infill) { if (have_infill) {
+1 -1
View File
@@ -161,7 +161,7 @@ void DailyTipsDataRenderer::render_img(const ImVec2& start_pos, const ImVec2& si
bool has_cjk(const std::string &text) 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 c1 = text[i];
unsigned char c2 = text[i + 1]; unsigned char c2 = text[i + 1];
unsigned char c3 = text[i + 2]; unsigned char c3 = text[i + 2];
+5 -3
View File
@@ -149,9 +149,9 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
{"sparse_infill_density", "", 1}, {"sparse_infill_density", "", 1},
{"fill_multiline", "", 1}, {"fill_multiline", "", 1},
{"sparse_infill_pattern", "", 1}, {"sparse_infill_pattern", "", 1},
{"tpms_adaptive", "", 1}, {"adaptive_infill_mode", "", 1},
{"tpms_interior_density", "", 1}, {"adaptive_infill_interior_density", "", 1},
{"tpms_adaptive_gradient", "", 1}, {"adaptive_infill_gradient", "", 1},
{"sparse_infill_smooth_factor", "", 1}, {"sparse_infill_smooth_factor", "", 1},
{"lateral_lattice_angle_1", "", 1}, {"lateral_lattice_angle_1", "", 1},
{"lateral_lattice_angle_2", "", 1}, {"lateral_lattice_angle_2", "", 1},
@@ -2158,6 +2158,7 @@ wxMenu* MenuFactory::multi_selection_menu()
menu->AppendSeparator(); menu->AppendSeparator();
append_menu_items_convert_unit(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_replace_all_with_stl(menu);
//BBS //BBS
append_menu_item_change_filament(menu); append_menu_item_change_filament(menu);
@@ -2173,6 +2174,7 @@ wxMenu* MenuFactory::multi_selection_menu()
//append_menu_item_simplify(menu); //append_menu_item_simplify(menu);
append_menu_item_delete(menu); append_menu_item_delete(menu);
append_menu_items_convert_unit(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_replace_all_with_stl(menu);
append_menu_item_change_filament(menu); append_menu_item_change_filament(menu);
wxMenu* split_menu = new wxMenu(); wxMenu* split_menu = new wxMenu();
+2 -2
View File
@@ -1089,8 +1089,8 @@ EmbossStyles GLGizmoEmboss::create_default_styles()
styles.push_back(WxFontUtils::create_emboss_style(wx_font, _u8L("First font"))); styles.push_back(WxFontUtils::create_emboss_style(wx_font, _u8L("First font")));
} else { } else {
// On current OS is not installed any correct TTF font // On current OS is not installed any correct TTF font
// use font packed with Slic3r // use font packed with OrcaSlicer
std::string font_path = Slic3r::resources_dir() + "/fonts/NotoSans-Regular.ttf"; 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}); styles.push_back(EmbossStyle{_u8L("Default font"), font_path, EmbossStyle::Type::file_path});
} }
return styles; return styles;
+1 -1
View File
@@ -33,7 +33,7 @@ static const char* HMS_LOCAL_IMG_PATH = "hms/local_image";
// the local HMS info // the local HMS info
// Orca: dev-id-type set trimmed to the devices Orca ships local HMS images for // Orca: dev-id-type set trimmed to the devices Orca ships local HMS images for
static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E"}; static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E", "20P", "26A", "31B"};
// Orca: HMS should be disabled when stealth mode is on or networking is not installed // Orca: HMS should be disabled when stealth mode is on or networking is not installed
static bool should_disable_hms() static bool should_disable_hms()
-2
View File
@@ -716,8 +716,6 @@ void PrintJob::process(Ctl &ctl)
} else { } else {
// wait for printer mqtt ready the same job id // wait for printer mqtt ready the same job id
wxGetApp().plater()->record_slice_preset("print");
BOOST_LOG_TRIVIAL(error) << "print_job: send ok."; BOOST_LOG_TRIVIAL(error) << "print_job: send ok.";
wxCommandEvent* evt = new wxCommandEvent(m_print_job_completed_id); wxCommandEvent* evt = new wxCommandEvent(m_print_job_completed_id);
if (!m_completed_evt_data.empty()) if (!m_completed_evt_data.empty())
+52 -72
View File
@@ -228,6 +228,7 @@
#include "Jobs/PlaterWorker.hpp" #include "Jobs/PlaterWorker.hpp"
#include "Jobs/BoostThreadWorker.hpp" #include "Jobs/BoostThreadWorker.hpp"
#include "BackgroundSlicingProcess.hpp" #include "BackgroundSlicingProcess.hpp"
#include "PostProcessor.hpp"
#include "SelectMachine.hpp" #include "SelectMachine.hpp"
#include "SendMultiMachinePage.hpp" #include "SendMultiMachinePage.hpp"
#include "SendToPrinter.hpp" #include "SendToPrinter.hpp"
@@ -17865,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}); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2)); print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2));
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false); set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional)); print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0)); print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
@@ -17929,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_min_speed", new ConfigOptionFloats { 0.0 });
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false}); filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false); set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional)); print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
print_config->set_key_value("wall_loops", new ConfigOptionInt(1)); print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0)); print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
@@ -18670,7 +18677,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect)
//BBS: remove GCodeViewer as seperate APP logic //BBS: remove GCodeViewer as seperate APP logic
bool Plater::load_files(const wxArrayString& filenames) 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); const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase);
std::vector<fs::path> normal_paths; std::vector<fs::path> normal_paths;
@@ -19572,7 +19579,11 @@ void Plater::export_gcode_3mf(bool export_all)
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
if (wxGetApp().app_config->get_bool("export_statistics")) if (wxGetApp().app_config->get_bool("export_statistics"))
strategy = strategy | SaveStrategy::WithStatistics; strategy = strategy | SaveStrategy::WithStatistics;
export_3mf(output_path, strategy, plate_idx); if (export_3mf(output_path, strategy, plate_idx) < 0) {
// No file was written (e.g. a post-processing script failed), don't report a successful export.
p->exporting_status = ExportingStatus::NOT_EXPORTING;
return;
}
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager(); RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
@@ -20429,6 +20440,45 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx, p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
strategy & SaveStrategy::WithStatistics); strategy & SaveStrategy::WithStatistics);
// Orca: non-BBL G-code isn't post-processed in place, so run the scripts on a copy before packing it.
std::vector<std::string> pp_tmp_files;
ScopeGuard remove_pp_tmp_files([&pp_tmp_files]() {
for (const std::string& f : pp_tmp_files) {
boost::system::error_code ec;
boost::filesystem::remove(f, ec);
}
});
// Backups (SkipStatic) don't pack G-code, so don't run user scripts for them.
if ((strategy & SaveStrategy::WithGcode) && !(strategy & SaveStrategy::SkipStatic) &&
p->printer_technology == ptFFF && !wxGetApp().preset_bundle->is_bbl_vendor()) {
try {
for (PlateData* pd : plate_data_list) {
if (pd == nullptr || pd->gcode_file.empty())
continue;
PartPlate* plate = p->partplate_list.get_plate(pd->plate_index);
if (plate == nullptr)
continue;
PrintBase* print = nullptr;
plate->get_print(&print, nullptr, nullptr);
Print* fff_print = dynamic_cast<Print*>(print);
if (fff_print == nullptr)
continue;
std::string src_path = pd->gcode_file; // in/out: becomes the post-processed copy if a script ran
std::string output_name = path_u8; // scripts see the real target; a rename is ignored for 3mf
if (run_post_process_scripts(src_path, true, "File", output_name, fff_print->full_print_config())) {
pp_tmp_files.push_back(src_path);
pd->gcode_file = src_path;
}
}
} catch (const std::exception& ex) {
release_PlateData_list(plate_data_list);
show_error(this, ex.what(), false);
return -1;
}
}
// BBS: backup // BBS: backup
PresetBundle& preset_bundle = *wxGetApp().preset_bundle; PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
std::vector<Preset*> project_presets = preset_bundle.get_current_project_embedded_presets(); std::vector<Preset*> project_presets = preset_bundle.get_current_project_embedded_presets();
@@ -20746,76 +20796,6 @@ void Plater::reslice()
p->preview->reload_print(); p->preview->reload_print();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index(); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index();
record_slice_preset("slicing");
}
void Plater::record_slice_preset(std::string action)
{
// record slice preset
try
{
json j;
auto printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset_with_vendor_profile().preset;
if (printer_preset.is_system) {
j["printer_preset_name"] = printer_preset.name;
}
else {
j["printer_preset_name"] = printer_preset.config.opt_string("inherits");
}
const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map;
if (keys_map) {
for (auto item : *keys_map) {
if (item.second == wxGetApp().preset_bundle->project_config.opt_enum<BedType>("curr_bed_type")) {
j["curr_bed_type"] = item.first;
break;
}
}
}
auto filament_presets = wxGetApp().preset_bundle->filament_presets;
for (int i = 0; i < filament_presets.size(); ++i) {
auto filament_preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[i]);
if (filament_preset->is_system) {
j["filament_preset_" + std::to_string(i)] = filament_preset->name;
}
else {
j["filament_preset_" + std::to_string(i)] = filament_preset->config.opt_string("inherits");
}
}
Preset& print_preset = wxGetApp().preset_bundle->prints.get_edited_preset();
if (print_preset.is_system) {
j["process_preset"] = print_preset.name;
}
else {
j["process_preset"] = print_preset.config.opt_string("inherits");
}
j["support_type"] = ConfigOptionEnum<SupportType>::get_enum_names().at(print_preset.config.opt_enum<SupportType>("support_type"));
j["sparse_infill_pattern"] = ConfigOptionEnum<InfillPattern>::get_enum_names().at(print_preset.config.opt_enum<InfillPattern>("sparse_infill_pattern"));
j["sparse_infill_density"] = print_preset.config.opt<ConfigOptionPercent>("sparse_infill_density")->value;
j["brim_type"] = ConfigOptionEnum<BrimType>::get_enum_names().at(print_preset.config.opt_enum<BrimType>("brim_type"));
j["user_mode"] = wxGetApp().get_mode_str();
if (p->background_process.fff_print()) {
const DynamicPrintConfig& full_config = p->background_process.fff_print()->full_print_config();
json values = json::array();
if (full_config.has("different_settings_to_system")) {
std::vector<std::string> different_values = full_config.option<ConfigOptionStrings>("different_settings_to_system")->values;
for (auto& item : different_values) {
values.push_back(item);
}
}
j["different_settings_to_system"] = values;
}
j["record_event"] = action;
NetworkAgent* agent = wxGetApp().getAgent();
}
catch (...)
{
return;
}
} }
//BBS: add project slicing related logic //BBS: add project slicing related logic
-1
View File
@@ -597,7 +597,6 @@ public:
bool has_toolpaths_to_export() const; bool has_toolpaths_to_export() const;
void export_toolpaths_to_obj() const; void export_toolpaths_to_obj() const;
void reslice(); void reslice();
void record_slice_preset(std::string action);
void reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages = false); 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_hollowing(const ModelObject &object, bool postpone_error_messages = false);
void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false); void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false);
+7 -3
View File
@@ -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 // 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 // extruder or a filament switch (both feed a single logical mapping surface), or when the plate is
// left/right split. Early-returns unless an item is actually in the wrong panel. // 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_) void SelectMachineDialog::update_material_item_pos(MachineObject* obj_)
{ {
if (!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 is_single_head = obj_->GetExtderSystem()->GetTotalExtderCount() < 2;
const bool has_switcher = obj_->GetFilaSwitch()->IsInstalled(); const bool has_switcher = obj_->GetFilaSwitch()->IsInstalled();
const bool use_single_panel = is_single_head || has_switcher; // reset_and_sync_ams_list() fills m_filaments_map only for a multi-nozzle plate
const auto& full_config = wxGetApp().preset_bundle->full_config();
const bool single_nozzle_plate = full_config.option<ConfigOptionFloats>("nozzle_diameter")->values.size() < 2;
const bool use_single_panel = is_single_head || has_switcher || single_nozzle_plate;
bool to_change_pos = false; bool to_change_pos = false;
for (const auto& iter : m_materialList) { for (const auto& iter : m_materialList) {
+3 -3
View File
@@ -2968,9 +2968,9 @@ void TabPrint::build()
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density"); optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline"); optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern"); optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("tpms_adaptive", "strength_settings_patterns#adaptive-density"); optgroup->append_single_option_line("adaptive_infill_mode", "strength_settings_patterns#adaptive-density");
optgroup->append_single_option_line("tpms_interior_density", "strength_settings_patterns#interior-density"); optgroup->append_single_option_line("adaptive_infill_interior_density", "strength_settings_patterns#interior-density");
optgroup->append_single_option_line("tpms_adaptive_gradient", "strength_settings_patterns#adaptive-gradient"); optgroup->append_single_option_line("adaptive_infill_gradient", "strength_settings_patterns#adaptive-gradient");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized"); optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor"); optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction"); optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
+11 -4
View File
@@ -7,6 +7,8 @@
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/DeviceCore/DevManager.h" #include "slic3r/GUI/DeviceCore/DevManager.h"
#include <algorithm>
#include <array>
#include <boost/format.hpp> #include <boost/format.hpp>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
@@ -16,6 +18,7 @@
#include <cmath> #include <cmath>
#include <slic3r/GUI/DeviceManager.hpp> #include <slic3r/GUI/DeviceManager.hpp>
#include <string> #include <string>
#include <string_view>
#include <utility> #include <utility>
#include <vector> #include <vector>
using json = nlohmann::json; using json = nlohmann::json;
@@ -64,12 +67,16 @@ const BambuFilamentIdMap& bambu_filament_id_map()
return map; return map;
} }
// Rewrites every string under "tray_info_idx", "filament_id" or "filamentId", at any depth, in place. // Keys whose string value is a filament id. "cate" and "fila_id" are the H2C nozzle rack's: the
// auto nozzle mapping request and the rack status in the push message.
constexpr std::array<std::string_view, 5> filament_id_keys{"tray_info_idx", "filament_id", "filamentId", "cate", "fila_id"};
// Rewrites every string under a filament id key, at any depth, in place.
void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::string>& map) void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::string>& map)
{ {
if (j.is_object()) { if (j.is_object()) {
for (auto& [key, value] : j.items()) { for (auto& [key, value] : j.items()) {
if (value.is_string() && (key == "tray_info_idx" || key == "filament_id" || key == "filamentId")) { if (value.is_string() && std::find(filament_id_keys.begin(), filament_id_keys.end(), key) != filament_id_keys.end()) {
auto it = map.find(value.get_ref<const std::string&>()); auto it = map.find(value.get_ref<const std::string&>());
if (it != map.end()) if (it != map.end())
value = it->second; value = it->second;
@@ -84,8 +91,8 @@ void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::st
// Text that does not parse as JSON, or that mentions none of the id keys, comes back byte-identical. // Text that does not parse as JSON, or that mentions none of the id keys, comes back byte-identical.
std::string rewrite_filament_ids(std::string text, const std::unordered_map<std::string, std::string>& map) std::string rewrite_filament_ids(std::string text, const std::unordered_map<std::string, std::string>& map)
{ {
if (map.empty() || (text.find("tray_info_idx") == std::string::npos && text.find("filament_id") == std::string::npos && if (map.empty() || std::none_of(filament_id_keys.begin(), filament_id_keys.end(),
text.find("filamentId") == std::string::npos)) [&text](std::string_view key) { return text.find(key) != std::string::npos; }))
return text; // nothing to map, skip the parse (moved, not copied) return text; // nothing to map, skip the parse (moved, not copied)
try { try {
json j = json::parse(text); json j = json::parse(text);
+2 -10
View File
@@ -1415,16 +1415,8 @@ void MoonrakerPrinterAgent::announce_printhost_device()
} }
} }
// Try to fetch actual device name from Moonraker // Never asks the printer, since discovery runs on the GUI thread and an unreachable printer would freeze it
// Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback const std::string dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
std::string dev_name;
MoonrakerDeviceInfo info;
std::string fetch_error;
if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) {
dev_name = info.dev_name;
} else {
dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
}
const std::string model_id = device_info.model_id; const std::string model_id = device_info.model_id;
+14 -5
View File
@@ -1917,13 +1917,20 @@ RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& rea
bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason) bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason)
{ {
bool needs_refresh = false; const auto needs_refresh = [&] {
{
std::lock_guard<std::mutex> lock(session_mutex); std::lock_guard<std::mutex> lock(session_mutex);
needs_refresh = should_refresh_locked(skew); return should_refresh_locked(skew);
} };
if (!needs_refresh())
return true;
if (!needs_refresh) // A refresh may already be running on another thread, e.g. the one start() launches. Wait for
// it and check again instead of spending the refresh token it is rotating. Bounded: the
// refresh POST times out after 30 s.
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
while (refresh_running.load() && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (!needs_refresh())
return true; return true;
// First attempt. refresh_from_storage blocks on the cross-process lock and reads the // First attempt. refresh_from_storage blocks on the cross-process lock and reads the
@@ -1935,6 +1942,8 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
// One retry, only for a transient network failure of the refresh POST (lost response, // One retry, only for a transient network failure of the refresh POST (lost response,
// timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store. // timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store.
std::this_thread::sleep_for(std::chrono::milliseconds(750)); std::this_thread::sleep_for(std::chrono::milliseconds(750));
if (!needs_refresh())
return true;
return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success; return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
} }
+10 -3
View File
@@ -11,6 +11,7 @@
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp> #include <boost/nowide/fstream.hpp>
#include <cstddef> #include <cstddef>
#include <future>
#include <memory> #include <memory>
#include <ios> #include <ios>
#include <slic3r/plugin/PluginDescriptor.hpp> #include <slic3r/plugin/PluginDescriptor.hpp>
@@ -49,16 +50,22 @@ bool CloudPluginService::fetch_manifests_into_descriptors(std::vector<PluginDesc
unauthorized.clear(); unauthorized.clear();
if (m_orca_agent) { if (m_orca_agent) {
// Refresh the token once, then run both requests in parallel.
m_orca_agent->ensure_token_fresh("fetch_plugin_manifests");
std::vector<PluginDescriptor> mine_descriptors;
auto mine = std::async(std::launch::async, [agent = m_orca_agent, &mine_descriptors] {
return agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
});
int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized); int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized);
const int mine_ret = mine.get();
if (ret != 0) { if (ret != 0) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins."; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins.";
return false; return false;
} }
std::vector<PluginDescriptor> mine_descriptors; ret = mine_ret;
ret = m_orca_agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
if (ret != 0) { if (ret != 0) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins."; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins.";
+6
View File
@@ -2,6 +2,7 @@
#include "GeneratedConfig.hpp" #include "GeneratedConfig.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "PluginAuditManager.hpp" #include "PluginAuditManager.hpp"
#include "PythonPluginBridge.hpp"
#include <atomic> #include <atomic>
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
@@ -664,6 +665,11 @@ bool PythonInterpreter::initialize()
// background-thread exceptions) to <data_dir>/log/python_*.log. // background-thread exceptions) to <data_dir>/log/python_*.log.
install_python_stderr_redirect(); install_python_stderr_redirect();
// Attribute plugin-spawned Python threads to the plugin/capability that created them.
// Must run while the GIL is still held here, after the audit hook is in place and
// before the GIL is released below.
install_python_thread_audit_shim();
// Release the GIL so other threads can acquire it via PyGILState_Ensure. // Release the GIL so other threads can acquire it via PyGILState_Ensure.
// Without this, calls from background threads will block trying to acquire the GIL. // Without this, calls from background threads will block trying to acquire the GIL.
m_main_thread_state = PyEval_SaveThread(); m_main_thread_state = PyEval_SaveThread();
+87
View File
@@ -603,6 +603,93 @@ request aborts the plugin load.)pbdoc");
} }
void install_python_thread_audit_shim()
{
// Runs with the GIL held from PythonInterpreter::initialize(). CPython's own thread bootstrap
// calls the target with no knowledge of the audit context that created it: the calling
// thread is the only place the plugin key and capability are still available. Capture them
// here, then re-establish the same ScopedPluginAuditContext inside the spawned thread before
// the plugin body runs. The identity lives only in this C++ closure -- no Python-visible
// setter is added.
try {
py::object thread_module = py::module_::import("_thread");
const int installed = PyObject_HasAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed");
if (installed == 1) {
BOOST_LOG_TRIVIAL(debug) << "[AUDIT] Python thread attribution shim already installed";
return;
}
if (installed < 0)
PyErr_Clear();
py::object original = py::reinterpret_steal<py::object>(
PyObject_GetAttrString(thread_module.ptr(), "start_new_thread"));
if (!original || !PyCallable_Check(original.ptr())) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error) << "[AUDIT] _thread.start_new_thread is missing or not callable; "
"plugin threads will run without audit attribution";
return;
}
py::object shim = py::cpp_function(
[original](py::object function, py::object args, py::object kwargs) -> py::object {
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
if (plugin_key.empty()) {
// Not called from a plugin: preserve the stock implementation untouched.
return original(function, args, kwargs);
}
const std::string capability = PluginAuditManager::instance().current_capability();
py::object guarded = py::cpp_function(
[function, plugin_key, capability](py::args call_args, py::kwargs call_kwargs) -> py::object {
ScopedPluginAuditContext audit_scope(plugin_key, capability);
PyObject* result = PyObject_Call(function.ptr(), call_args.ptr(), call_kwargs.ptr());
if (result == nullptr)
throw py::error_already_set();
return py::reinterpret_steal<py::object>(result);
});
return original(guarded, args, kwargs);
},
py::arg("function"), py::arg("args"), py::arg("kwargs") = py::none());
if (PyObject_SetAttrString(thread_module.ptr(), "start_new_thread", shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error)
<< "[AUDIT] Failed to install Python thread attribution shim on _thread.start_new_thread";
return;
}
if (PyObject_SetAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed", Py_True) < 0)
PyErr_Clear();
// threading.py caches the function at import time (as `_start_new_thread` in 3.12, and as
// `start_new_thread` in older versions). Patch the cached reference too when the module is
// already loaded; otherwise its Thread.start() would keep calling the unwrapped original.
py::object sys_modules = py::module_::import("sys").attr("modules");
PyObject* threading_module = PyDict_GetItemString(sys_modules.ptr(), "threading");
if (threading_module && threading_module != Py_None) {
const char* cached_names[] = {"_start_new_thread", "start_new_thread"};
for (const char* cached_name : cached_names) {
const int has_cached_name = PyObject_HasAttrString(threading_module, cached_name);
if (has_cached_name < 0) {
PyErr_Clear();
continue;
}
if (has_cached_name != 1)
continue;
if (PyObject_SetAttrString(threading_module, cached_name, shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] Failed to update threading." << cached_name
<< " with the Python thread attribution shim";
}
}
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Python thread attribution shim installed";
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install Python thread attribution shim: " << ex.what();
}
}
} // namespace Slic3r } // namespace Slic3r
#ifdef ORCA_PYTHON_STUBGEN_MODULE #ifdef ORCA_PYTHON_STUBGEN_MODULE
+9
View File
@@ -13,6 +13,15 @@ namespace Slic3r { class PluginCapabilityInterface; }
namespace Slic3r { namespace Slic3r {
// Install the thread-attribution shim: wraps `_thread.start_new_thread` (and the reference
// cached by an already-imported `threading` module) so a thread spawned from inside a plugin
// call runs with that call's plugin key/capability audit scope. The captured identity is held
// in a private C++ wrapper; nothing is exposed to Python that could set it.
//
// Call with the GIL held, after PluginAuditManager::install_hook() and before the interpreter
// releases the GIL. Safe to call more than once per interpreter.
void install_python_thread_audit_shim();
// One materialized capability returned from finalize_plugin_capture: the C++ instance // One materialized capability returned from finalize_plugin_capture: the C++ instance
// and its resolved get_name() (cached while the GIL was held). // and its resolved get_name() (cached while the GIL was held).
struct CapturedCapability struct CapturedCapability
+20
View File
@@ -0,0 +1,20 @@
`two_color_jpeg.glb` is a generated, closed tetrahedron with four faces, UVs, and
an embedded 8×8 red/blue JPEG base-color texture. Its coordinates span 20 mm.
The core tests load all three fixtures through `Model::read_from_file`, convert
textures or MTL colors to painting with boundary smoothing, and match red and blue
to filaments. They run in CI without a display. A separate layout check compares
the smoothing search-tree type seen by a core consumer with the same type compiled
inside `libslic3r_cgal`. The mismatch is checked directly because undefined behavior
can be masked by which inline template implementations the linker selects.
`two_color_sphere/test_two_color_sphere.obj` and its matching MTL are the original
red/blue material-color sphere supplied by RF47 in
[the PR discussion](https://github.com/OrcaSlicer/OrcaSlicer/pull/16607#issuecomment-6244475095).
They contain solid `Kd` colors and no texture images.
`test_two_color_sphere_textured.obj` is a derived version of that sphere. Its MTL
uses white `Kd` values and references `textures/two_color.jpg` by a relative path;
the two face groups sample different halves of that image through their UVs.
The JPEG is extracted unchanged from `two_color_jpeg.glb`. This case must retain
both colors from the external texture, rather than fall back to MTL colors.
Binary file not shown.
@@ -0,0 +1,11 @@
# test model for OrcaSlicer tex2color
newmtl Red
Ka 0.0000 0.0000 0.0000
Kd 0.7500 0.2500 0.2500
Ks 0.0000 0.0000 0.0000
d 1.0000
newmtl Blue
Ka 0.0000 0.0000 0.0000
Kd 0.0700 0.2100 0.7000
Ks 0.0000 0.0000 0.0000
d 1.0000
@@ -0,0 +1,457 @@
# test model for OrcaSlicer tex2color
mtllib test_two_color_sphere.mtl
v 0.000000 0.000000 20.000000
v 7.653669 0.000000 18.477591
v 7.071068 2.928932 18.477591
v 5.411961 5.411961 18.477591
v 2.928932 7.071068 18.477591
v 0.000000 7.653669 18.477591
v -2.928932 7.071068 18.477591
v -5.411961 5.411961 18.477591
v -7.071068 2.928932 18.477591
v -7.653669 0.000000 18.477591
v -7.071068 -2.928932 18.477591
v -5.411961 -5.411961 18.477591
v -2.928932 -7.071068 18.477591
v -0.000000 -7.653669 18.477591
v 2.928932 -7.071068 18.477591
v 5.411961 -5.411961 18.477591
v 7.071068 -2.928932 18.477591
v 14.142136 0.000000 14.142136
v 13.065630 5.411961 14.142136
v 10.000000 10.000000 14.142136
v 5.411961 13.065630 14.142136
v 0.000000 14.142136 14.142136
v -5.411961 13.065630 14.142136
v -10.000000 10.000000 14.142136
v -13.065630 5.411961 14.142136
v -14.142136 0.000000 14.142136
v -13.065630 -5.411961 14.142136
v -10.000000 -10.000000 14.142136
v -5.411961 -13.065630 14.142136
v -0.000000 -14.142136 14.142136
v 5.411961 -13.065630 14.142136
v 10.000000 -10.000000 14.142136
v 13.065630 -5.411961 14.142136
v 18.477591 0.000000 7.653669
v 17.071068 7.071068 7.653669
v 13.065630 13.065630 7.653669
v 7.071068 17.071068 7.653669
v 0.000000 18.477591 7.653669
v -7.071068 17.071068 7.653669
v -13.065630 13.065630 7.653669
v -17.071068 7.071068 7.653669
v -18.477591 0.000000 7.653669
v -17.071068 -7.071068 7.653669
v -13.065630 -13.065630 7.653669
v -7.071068 -17.071068 7.653669
v -0.000000 -18.477591 7.653669
v 7.071068 -17.071068 7.653669
v 13.065630 -13.065630 7.653669
v 17.071068 -7.071068 7.653669
v 20.000000 0.000000 0.000000
v 18.477591 7.653669 0.000000
v 14.142136 14.142136 0.000000
v 7.653669 18.477591 0.000000
v 0.000000 20.000000 0.000000
v -7.653669 18.477591 0.000000
v -14.142136 14.142136 0.000000
v -18.477591 7.653669 0.000000
v -20.000000 0.000000 0.000000
v -18.477591 -7.653669 0.000000
v -14.142136 -14.142136 0.000000
v -7.653669 -18.477591 0.000000
v -0.000000 -20.000000 0.000000
v 7.653669 -18.477591 0.000000
v 14.142136 -14.142136 0.000000
v 18.477591 -7.653669 0.000000
v 18.477591 0.000000 -7.653669
v 17.071068 7.071068 -7.653669
v 13.065630 13.065630 -7.653669
v 7.071068 17.071068 -7.653669
v 0.000000 18.477591 -7.653669
v -7.071068 17.071068 -7.653669
v -13.065630 13.065630 -7.653669
v -17.071068 7.071068 -7.653669
v -18.477591 0.000000 -7.653669
v -17.071068 -7.071068 -7.653669
v -13.065630 -13.065630 -7.653669
v -7.071068 -17.071068 -7.653669
v -0.000000 -18.477591 -7.653669
v 7.071068 -17.071068 -7.653669
v 13.065630 -13.065630 -7.653669
v 17.071068 -7.071068 -7.653669
v 14.142136 0.000000 -14.142136
v 13.065630 5.411961 -14.142136
v 10.000000 10.000000 -14.142136
v 5.411961 13.065630 -14.142136
v 0.000000 14.142136 -14.142136
v -5.411961 13.065630 -14.142136
v -10.000000 10.000000 -14.142136
v -13.065630 5.411961 -14.142136
v -14.142136 0.000000 -14.142136
v -13.065630 -5.411961 -14.142136
v -10.000000 -10.000000 -14.142136
v -5.411961 -13.065630 -14.142136
v -0.000000 -14.142136 -14.142136
v 5.411961 -13.065630 -14.142136
v 10.000000 -10.000000 -14.142136
v 13.065630 -5.411961 -14.142136
v 7.653669 0.000000 -18.477591
v 7.071068 2.928932 -18.477591
v 5.411961 5.411961 -18.477591
v 2.928932 7.071068 -18.477591
v 0.000000 7.653669 -18.477591
v -2.928932 7.071068 -18.477591
v -5.411961 5.411961 -18.477591
v -7.071068 2.928932 -18.477591
v -7.653669 0.000000 -18.477591
v -7.071068 -2.928932 -18.477591
v -5.411961 -5.411961 -18.477591
v -2.928932 -7.071068 -18.477591
v -0.000000 -7.653669 -18.477591
v 2.928932 -7.071068 -18.477591
v 5.411961 -5.411961 -18.477591
v 7.071068 -2.928932 -18.477591
v 0.000000 0.000000 -20.000000
vt 0 0
vn 0.000000 -0.000000 1.000000
vn 0.439605 0.035510 0.897489
vn 0.392553 0.201037 0.897489
vn 0.285739 0.335958 0.897489
vn 0.135423 0.419732 0.897489
vn -0.035510 0.439605 0.897489
vn -0.201037 0.392553 0.897489
vn -0.335958 0.285739 0.897489
vn -0.419732 0.135423 0.897489
vn -0.439605 -0.035510 0.897489
vn -0.392553 -0.201037 0.897489
vn -0.285739 -0.335958 0.897489
vn -0.135423 -0.419732 0.897489
vn 0.035510 -0.439605 0.897489
vn 0.201037 -0.392553 0.897489
vn 0.335958 -0.285739 0.897489
vn 0.419732 -0.135423 0.897489
vn 0.725001 0.027230 0.688209
vn 0.659393 0.302603 0.688209
vn 0.493399 0.531907 0.688209
vn 0.252289 0.680234 0.688209
vn -0.027230 0.725001 0.688209
vn -0.302603 0.659393 0.688209
vn -0.531907 0.493399 0.688209
vn -0.680234 0.252289 0.688209
vn -0.725001 -0.027230 0.688209
vn -0.659393 -0.302603 0.688209
vn -0.493399 -0.531907 0.688209
vn -0.252289 -0.680234 0.688209
vn 0.027230 -0.725001 0.688209
vn 0.302603 -0.659393 0.688209
vn 0.531907 -0.493399 0.688209
vn 0.680234 -0.252289 0.688209
vn 0.927851 0.014745 0.372658
vn 0.851580 0.368696 0.372658
vn 0.645664 0.666516 0.372658
vn 0.341451 0.862865 0.372658
vn -0.014745 0.927851 0.372658
vn -0.368696 0.851580 0.372658
vn -0.666516 0.645664 0.372658
vn -0.862865 0.341451 0.372658
vn -0.927851 -0.014745 0.372658
vn -0.851580 -0.368696 0.372658
vn -0.645664 -0.666516 0.372658
vn -0.341451 -0.862865 0.372658
vn 0.014745 -0.927851 0.372658
vn 0.368696 -0.851580 0.372658
vn 0.666516 -0.645664 0.372658
vn 0.862865 -0.341451 0.372658
vn 1.000000 -0.000000 -0.000000
vn 0.923880 0.382683 0.000000
vn 0.707107 0.707107 0.000000
vn 0.382683 0.923880 0.000000
vn 0.000000 1.000000 0.000000
vn -0.382683 0.923880 0.000000
vn -0.707107 0.707107 0.000000
vn -0.923880 0.382683 0.000000
vn -1.000000 0.000000 0.000000
vn -0.923880 -0.382683 -0.000000
vn -0.707107 -0.707107 0.000000
vn -0.382683 -0.923880 -0.000000
vn -0.000000 -1.000000 -0.000000
vn 0.382683 -0.923880 0.000000
vn 0.707107 -0.707107 0.000000
vn 0.923880 -0.382683 -0.000000
vn 0.927851 -0.014745 -0.372658
vn 0.862865 0.341451 -0.372658
vn 0.666516 0.645664 -0.372658
vn 0.368696 0.851580 -0.372658
vn 0.014745 0.927851 -0.372658
vn -0.341451 0.862865 -0.372658
vn -0.645664 0.666516 -0.372658
vn -0.851580 0.368696 -0.372658
vn -0.927851 0.014745 -0.372658
vn -0.862865 -0.341451 -0.372658
vn -0.666516 -0.645664 -0.372658
vn -0.368696 -0.851580 -0.372658
vn -0.014745 -0.927851 -0.372658
vn 0.341451 -0.862865 -0.372658
vn 0.645664 -0.666516 -0.372658
vn 0.851580 -0.368696 -0.372658
vn 0.725001 -0.027230 -0.688209
vn 0.680234 0.252289 -0.688209
vn 0.531907 0.493399 -0.688209
vn 0.302603 0.659393 -0.688209
vn 0.027230 0.725001 -0.688209
vn -0.252289 0.680234 -0.688209
vn -0.493399 0.531907 -0.688209
vn -0.659393 0.302603 -0.688209
vn -0.725001 0.027230 -0.688209
vn -0.680234 -0.252289 -0.688209
vn -0.531907 -0.493399 -0.688209
vn -0.302603 -0.659393 -0.688209
vn -0.027230 -0.725001 -0.688209
vn 0.252289 -0.680234 -0.688209
vn 0.493399 -0.531907 -0.688209
vn 0.659393 -0.302603 -0.688209
vn 0.439605 -0.035510 -0.897489
vn 0.419732 0.135423 -0.897489
vn 0.335958 0.285739 -0.897489
vn 0.201037 0.392553 -0.897489
vn 0.035510 0.439605 -0.897489
vn -0.135423 0.419732 -0.897489
vn -0.285739 0.335958 -0.897489
vn -0.392553 0.201037 -0.897489
vn -0.439605 0.035510 -0.897489
vn -0.419732 -0.135423 -0.897489
vn -0.335958 -0.285739 -0.897489
vn -0.201037 -0.392553 -0.897489
vn -0.035510 -0.439605 -0.897489
vn 0.135423 -0.419732 -0.897489
vn 0.285739 -0.335958 -0.897489
vn 0.392553 -0.201037 -0.897489
vn 0.000000 -0.000000 -1.000000
usemtl Red
f 1/1/1 2/1/2 3/1/3
f 1/1/1 3/1/3 4/1/4
f 1/1/1 4/1/4 5/1/5
f 1/1/1 5/1/5 6/1/6
f 1/1/1 6/1/6 7/1/7
f 1/1/1 7/1/7 8/1/8
f 1/1/1 8/1/8 9/1/9
f 1/1/1 9/1/9 10/1/10
f 1/1/1 10/1/10 11/1/11
f 1/1/1 11/1/11 12/1/12
f 1/1/1 12/1/12 13/1/13
f 1/1/1 13/1/13 14/1/14
f 1/1/1 14/1/14 15/1/15
f 1/1/1 15/1/15 16/1/16
f 1/1/1 16/1/16 17/1/17
f 1/1/1 17/1/17 2/1/2
f 2/1/2 18/1/18 19/1/19
f 2/1/2 19/1/19 3/1/3
f 3/1/3 19/1/19 20/1/20
f 3/1/3 20/1/20 4/1/4
f 4/1/4 20/1/20 21/1/21
f 4/1/4 21/1/21 5/1/5
f 5/1/5 21/1/21 22/1/22
f 5/1/5 22/1/22 6/1/6
f 6/1/6 22/1/22 23/1/23
f 6/1/6 23/1/23 7/1/7
f 7/1/7 23/1/23 24/1/24
f 7/1/7 24/1/24 8/1/8
f 8/1/8 24/1/24 25/1/25
f 8/1/8 25/1/25 9/1/9
f 9/1/9 25/1/25 26/1/26
f 9/1/9 26/1/26 10/1/10
f 10/1/10 26/1/26 27/1/27
f 10/1/10 27/1/27 11/1/11
f 11/1/11 27/1/27 28/1/28
f 11/1/11 28/1/28 12/1/12
f 12/1/12 28/1/28 29/1/29
f 12/1/12 29/1/29 13/1/13
f 13/1/13 29/1/29 30/1/30
f 13/1/13 30/1/30 14/1/14
f 14/1/14 30/1/30 31/1/31
f 14/1/14 31/1/31 15/1/15
f 15/1/15 31/1/31 32/1/32
f 15/1/15 32/1/32 16/1/16
f 16/1/16 32/1/32 33/1/33
f 16/1/16 33/1/33 17/1/17
f 17/1/17 33/1/33 18/1/18
f 17/1/17 18/1/18 2/1/2
f 18/1/18 34/1/34 35/1/35
f 18/1/18 35/1/35 19/1/19
f 19/1/19 35/1/35 36/1/36
f 19/1/19 36/1/36 20/1/20
f 20/1/20 36/1/36 37/1/37
f 20/1/20 37/1/37 21/1/21
f 21/1/21 37/1/37 38/1/38
f 21/1/21 38/1/38 22/1/22
f 22/1/22 38/1/38 39/1/39
f 22/1/22 39/1/39 23/1/23
f 23/1/23 39/1/39 40/1/40
f 23/1/23 40/1/40 24/1/24
f 24/1/24 40/1/40 41/1/41
f 24/1/24 41/1/41 25/1/25
f 25/1/25 41/1/41 42/1/42
f 25/1/25 42/1/42 26/1/26
f 26/1/26 42/1/42 43/1/43
f 26/1/26 43/1/43 27/1/27
f 27/1/27 43/1/43 44/1/44
f 27/1/27 44/1/44 28/1/28
f 28/1/28 44/1/44 45/1/45
f 28/1/28 45/1/45 29/1/29
f 29/1/29 45/1/45 46/1/46
f 29/1/29 46/1/46 30/1/30
f 30/1/30 46/1/46 47/1/47
f 30/1/30 47/1/47 31/1/31
f 31/1/31 47/1/47 48/1/48
f 31/1/31 48/1/48 32/1/32
f 32/1/32 48/1/48 49/1/49
f 32/1/32 49/1/49 33/1/33
f 33/1/33 49/1/49 34/1/34
f 33/1/33 34/1/34 18/1/18
f 34/1/34 50/1/50 51/1/51
f 34/1/34 51/1/51 35/1/35
f 35/1/35 51/1/51 52/1/52
f 35/1/35 52/1/52 36/1/36
f 36/1/36 52/1/52 53/1/53
f 36/1/36 53/1/53 37/1/37
f 37/1/37 53/1/53 54/1/54
f 37/1/37 54/1/54 38/1/38
f 38/1/38 54/1/54 55/1/55
f 38/1/38 55/1/55 39/1/39
f 39/1/39 55/1/55 56/1/56
f 39/1/39 56/1/56 40/1/40
f 40/1/40 56/1/56 57/1/57
f 40/1/40 57/1/57 41/1/41
f 41/1/41 57/1/57 58/1/58
f 41/1/41 58/1/58 42/1/42
f 42/1/42 58/1/58 59/1/59
f 42/1/42 59/1/59 43/1/43
f 43/1/43 59/1/59 60/1/60
f 43/1/43 60/1/60 44/1/44
f 44/1/44 60/1/60 61/1/61
f 44/1/44 61/1/61 45/1/45
f 45/1/45 61/1/61 62/1/62
f 45/1/45 62/1/62 46/1/46
f 46/1/46 62/1/62 63/1/63
f 46/1/46 63/1/63 47/1/47
f 47/1/47 63/1/63 64/1/64
f 47/1/47 64/1/64 48/1/48
f 48/1/48 64/1/64 65/1/65
f 48/1/48 65/1/65 49/1/49
f 49/1/49 65/1/65 50/1/50
f 49/1/49 50/1/50 34/1/34
usemtl Blue
f 50/1/50 66/1/66 67/1/67
f 50/1/50 67/1/67 51/1/51
f 51/1/51 67/1/67 68/1/68
f 51/1/51 68/1/68 52/1/52
f 52/1/52 68/1/68 69/1/69
f 52/1/52 69/1/69 53/1/53
f 53/1/53 69/1/69 70/1/70
f 53/1/53 70/1/70 54/1/54
f 54/1/54 70/1/70 71/1/71
f 54/1/54 71/1/71 55/1/55
f 55/1/55 71/1/71 72/1/72
f 55/1/55 72/1/72 56/1/56
f 56/1/56 72/1/72 73/1/73
f 56/1/56 73/1/73 57/1/57
f 57/1/57 73/1/73 74/1/74
f 57/1/57 74/1/74 58/1/58
f 58/1/58 74/1/74 75/1/75
f 58/1/58 75/1/75 59/1/59
f 59/1/59 75/1/75 76/1/76
f 59/1/59 76/1/76 60/1/60
f 60/1/60 76/1/76 77/1/77
f 60/1/60 77/1/77 61/1/61
f 61/1/61 77/1/77 78/1/78
f 61/1/61 78/1/78 62/1/62
f 62/1/62 78/1/78 79/1/79
f 62/1/62 79/1/79 63/1/63
f 63/1/63 79/1/79 80/1/80
f 63/1/63 80/1/80 64/1/64
f 64/1/64 80/1/80 81/1/81
f 64/1/64 81/1/81 65/1/65
f 65/1/65 81/1/81 66/1/66
f 65/1/65 66/1/66 50/1/50
f 66/1/66 82/1/82 83/1/83
f 66/1/66 83/1/83 67/1/67
f 67/1/67 83/1/83 84/1/84
f 67/1/67 84/1/84 68/1/68
f 68/1/68 84/1/84 85/1/85
f 68/1/68 85/1/85 69/1/69
f 69/1/69 85/1/85 86/1/86
f 69/1/69 86/1/86 70/1/70
f 70/1/70 86/1/86 87/1/87
f 70/1/70 87/1/87 71/1/71
f 71/1/71 87/1/87 88/1/88
f 71/1/71 88/1/88 72/1/72
f 72/1/72 88/1/88 89/1/89
f 72/1/72 89/1/89 73/1/73
f 73/1/73 89/1/89 90/1/90
f 73/1/73 90/1/90 74/1/74
f 74/1/74 90/1/90 91/1/91
f 74/1/74 91/1/91 75/1/75
f 75/1/75 91/1/91 92/1/92
f 75/1/75 92/1/92 76/1/76
f 76/1/76 92/1/92 93/1/93
f 76/1/76 93/1/93 77/1/77
f 77/1/77 93/1/93 94/1/94
f 77/1/77 94/1/94 78/1/78
f 78/1/78 94/1/94 95/1/95
f 78/1/78 95/1/95 79/1/79
f 79/1/79 95/1/95 96/1/96
f 79/1/79 96/1/96 80/1/80
f 80/1/80 96/1/96 97/1/97
f 80/1/80 97/1/97 81/1/81
f 81/1/81 97/1/97 82/1/82
f 81/1/81 82/1/82 66/1/66
f 82/1/82 98/1/98 99/1/99
f 82/1/82 99/1/99 83/1/83
f 83/1/83 99/1/99 100/1/100
f 83/1/83 100/1/100 84/1/84
f 84/1/84 100/1/100 101/1/101
f 84/1/84 101/1/101 85/1/85
f 85/1/85 101/1/101 102/1/102
f 85/1/85 102/1/102 86/1/86
f 86/1/86 102/1/102 103/1/103
f 86/1/86 103/1/103 87/1/87
f 87/1/87 103/1/103 104/1/104
f 87/1/87 104/1/104 88/1/88
f 88/1/88 104/1/104 105/1/105
f 88/1/88 105/1/105 89/1/89
f 89/1/89 105/1/105 106/1/106
f 89/1/89 106/1/106 90/1/90
f 90/1/90 106/1/106 107/1/107
f 90/1/90 107/1/107 91/1/91
f 91/1/91 107/1/107 108/1/108
f 91/1/91 108/1/108 92/1/92
f 92/1/92 108/1/108 109/1/109
f 92/1/92 109/1/109 93/1/93
f 93/1/93 109/1/109 110/1/110
f 93/1/93 110/1/110 94/1/94
f 94/1/94 110/1/110 111/1/111
f 94/1/94 111/1/111 95/1/95
f 95/1/95 111/1/111 112/1/112
f 95/1/95 112/1/112 96/1/96
f 96/1/96 112/1/112 113/1/113
f 96/1/96 113/1/113 97/1/97
f 97/1/97 113/1/113 98/1/98
f 97/1/97 98/1/98 82/1/82
f 114/1/114 99/1/99 98/1/98
f 114/1/114 100/1/100 99/1/99
f 114/1/114 101/1/101 100/1/100
f 114/1/114 102/1/102 101/1/101
f 114/1/114 103/1/103 102/1/102
f 114/1/114 104/1/104 103/1/103
f 114/1/114 105/1/105 104/1/104
f 114/1/114 106/1/106 105/1/105
f 114/1/114 107/1/107 106/1/106
f 114/1/114 108/1/108 107/1/107
f 114/1/114 109/1/109 108/1/108
f 114/1/114 110/1/110 109/1/109
f 114/1/114 111/1/111 110/1/110
f 114/1/114 112/1/112 111/1/111
f 114/1/114 113/1/113 112/1/112
f 114/1/114 98/1/98 113/1/113
@@ -0,0 +1,8 @@
# Derived texture-file regression: color comes from map_Kd, not Kd.
newmtl Red
Kd 1 1 1
map_Kd textures/two_color.jpg
newmtl Blue
Kd 1 1 1
map_Kd textures/two_color.jpg
@@ -0,0 +1,458 @@
# test model for OrcaSlicer tex2color
mtllib test_two_color_sphere_textured.mtl
v 0.000000 0.000000 20.000000
v 7.653669 0.000000 18.477591
v 7.071068 2.928932 18.477591
v 5.411961 5.411961 18.477591
v 2.928932 7.071068 18.477591
v 0.000000 7.653669 18.477591
v -2.928932 7.071068 18.477591
v -5.411961 5.411961 18.477591
v -7.071068 2.928932 18.477591
v -7.653669 0.000000 18.477591
v -7.071068 -2.928932 18.477591
v -5.411961 -5.411961 18.477591
v -2.928932 -7.071068 18.477591
v -0.000000 -7.653669 18.477591
v 2.928932 -7.071068 18.477591
v 5.411961 -5.411961 18.477591
v 7.071068 -2.928932 18.477591
v 14.142136 0.000000 14.142136
v 13.065630 5.411961 14.142136
v 10.000000 10.000000 14.142136
v 5.411961 13.065630 14.142136
v 0.000000 14.142136 14.142136
v -5.411961 13.065630 14.142136
v -10.000000 10.000000 14.142136
v -13.065630 5.411961 14.142136
v -14.142136 0.000000 14.142136
v -13.065630 -5.411961 14.142136
v -10.000000 -10.000000 14.142136
v -5.411961 -13.065630 14.142136
v -0.000000 -14.142136 14.142136
v 5.411961 -13.065630 14.142136
v 10.000000 -10.000000 14.142136
v 13.065630 -5.411961 14.142136
v 18.477591 0.000000 7.653669
v 17.071068 7.071068 7.653669
v 13.065630 13.065630 7.653669
v 7.071068 17.071068 7.653669
v 0.000000 18.477591 7.653669
v -7.071068 17.071068 7.653669
v -13.065630 13.065630 7.653669
v -17.071068 7.071068 7.653669
v -18.477591 0.000000 7.653669
v -17.071068 -7.071068 7.653669
v -13.065630 -13.065630 7.653669
v -7.071068 -17.071068 7.653669
v -0.000000 -18.477591 7.653669
v 7.071068 -17.071068 7.653669
v 13.065630 -13.065630 7.653669
v 17.071068 -7.071068 7.653669
v 20.000000 0.000000 0.000000
v 18.477591 7.653669 0.000000
v 14.142136 14.142136 0.000000
v 7.653669 18.477591 0.000000
v 0.000000 20.000000 0.000000
v -7.653669 18.477591 0.000000
v -14.142136 14.142136 0.000000
v -18.477591 7.653669 0.000000
v -20.000000 0.000000 0.000000
v -18.477591 -7.653669 0.000000
v -14.142136 -14.142136 0.000000
v -7.653669 -18.477591 0.000000
v -0.000000 -20.000000 0.000000
v 7.653669 -18.477591 0.000000
v 14.142136 -14.142136 0.000000
v 18.477591 -7.653669 0.000000
v 18.477591 0.000000 -7.653669
v 17.071068 7.071068 -7.653669
v 13.065630 13.065630 -7.653669
v 7.071068 17.071068 -7.653669
v 0.000000 18.477591 -7.653669
v -7.071068 17.071068 -7.653669
v -13.065630 13.065630 -7.653669
v -17.071068 7.071068 -7.653669
v -18.477591 0.000000 -7.653669
v -17.071068 -7.071068 -7.653669
v -13.065630 -13.065630 -7.653669
v -7.071068 -17.071068 -7.653669
v -0.000000 -18.477591 -7.653669
v 7.071068 -17.071068 -7.653669
v 13.065630 -13.065630 -7.653669
v 17.071068 -7.071068 -7.653669
v 14.142136 0.000000 -14.142136
v 13.065630 5.411961 -14.142136
v 10.000000 10.000000 -14.142136
v 5.411961 13.065630 -14.142136
v 0.000000 14.142136 -14.142136
v -5.411961 13.065630 -14.142136
v -10.000000 10.000000 -14.142136
v -13.065630 5.411961 -14.142136
v -14.142136 0.000000 -14.142136
v -13.065630 -5.411961 -14.142136
v -10.000000 -10.000000 -14.142136
v -5.411961 -13.065630 -14.142136
v -0.000000 -14.142136 -14.142136
v 5.411961 -13.065630 -14.142136
v 10.000000 -10.000000 -14.142136
v 13.065630 -5.411961 -14.142136
v 7.653669 0.000000 -18.477591
v 7.071068 2.928932 -18.477591
v 5.411961 5.411961 -18.477591
v 2.928932 7.071068 -18.477591
v 0.000000 7.653669 -18.477591
v -2.928932 7.071068 -18.477591
v -5.411961 5.411961 -18.477591
v -7.071068 2.928932 -18.477591
v -7.653669 0.000000 -18.477591
v -7.071068 -2.928932 -18.477591
v -5.411961 -5.411961 -18.477591
v -2.928932 -7.071068 -18.477591
v -0.000000 -7.653669 -18.477591
v 2.928932 -7.071068 -18.477591
v 5.411961 -5.411961 -18.477591
v 7.071068 -2.928932 -18.477591
v 0.000000 0.000000 -20.000000
vt 0.25 0.5
vt 0.75 0.5
vn 0.000000 -0.000000 1.000000
vn 0.439605 0.035510 0.897489
vn 0.392553 0.201037 0.897489
vn 0.285739 0.335958 0.897489
vn 0.135423 0.419732 0.897489
vn -0.035510 0.439605 0.897489
vn -0.201037 0.392553 0.897489
vn -0.335958 0.285739 0.897489
vn -0.419732 0.135423 0.897489
vn -0.439605 -0.035510 0.897489
vn -0.392553 -0.201037 0.897489
vn -0.285739 -0.335958 0.897489
vn -0.135423 -0.419732 0.897489
vn 0.035510 -0.439605 0.897489
vn 0.201037 -0.392553 0.897489
vn 0.335958 -0.285739 0.897489
vn 0.419732 -0.135423 0.897489
vn 0.725001 0.027230 0.688209
vn 0.659393 0.302603 0.688209
vn 0.493399 0.531907 0.688209
vn 0.252289 0.680234 0.688209
vn -0.027230 0.725001 0.688209
vn -0.302603 0.659393 0.688209
vn -0.531907 0.493399 0.688209
vn -0.680234 0.252289 0.688209
vn -0.725001 -0.027230 0.688209
vn -0.659393 -0.302603 0.688209
vn -0.493399 -0.531907 0.688209
vn -0.252289 -0.680234 0.688209
vn 0.027230 -0.725001 0.688209
vn 0.302603 -0.659393 0.688209
vn 0.531907 -0.493399 0.688209
vn 0.680234 -0.252289 0.688209
vn 0.927851 0.014745 0.372658
vn 0.851580 0.368696 0.372658
vn 0.645664 0.666516 0.372658
vn 0.341451 0.862865 0.372658
vn -0.014745 0.927851 0.372658
vn -0.368696 0.851580 0.372658
vn -0.666516 0.645664 0.372658
vn -0.862865 0.341451 0.372658
vn -0.927851 -0.014745 0.372658
vn -0.851580 -0.368696 0.372658
vn -0.645664 -0.666516 0.372658
vn -0.341451 -0.862865 0.372658
vn 0.014745 -0.927851 0.372658
vn 0.368696 -0.851580 0.372658
vn 0.666516 -0.645664 0.372658
vn 0.862865 -0.341451 0.372658
vn 1.000000 -0.000000 -0.000000
vn 0.923880 0.382683 0.000000
vn 0.707107 0.707107 0.000000
vn 0.382683 0.923880 0.000000
vn 0.000000 1.000000 0.000000
vn -0.382683 0.923880 0.000000
vn -0.707107 0.707107 0.000000
vn -0.923880 0.382683 0.000000
vn -1.000000 0.000000 0.000000
vn -0.923880 -0.382683 -0.000000
vn -0.707107 -0.707107 0.000000
vn -0.382683 -0.923880 -0.000000
vn -0.000000 -1.000000 -0.000000
vn 0.382683 -0.923880 0.000000
vn 0.707107 -0.707107 0.000000
vn 0.923880 -0.382683 -0.000000
vn 0.927851 -0.014745 -0.372658
vn 0.862865 0.341451 -0.372658
vn 0.666516 0.645664 -0.372658
vn 0.368696 0.851580 -0.372658
vn 0.014745 0.927851 -0.372658
vn -0.341451 0.862865 -0.372658
vn -0.645664 0.666516 -0.372658
vn -0.851580 0.368696 -0.372658
vn -0.927851 0.014745 -0.372658
vn -0.862865 -0.341451 -0.372658
vn -0.666516 -0.645664 -0.372658
vn -0.368696 -0.851580 -0.372658
vn -0.014745 -0.927851 -0.372658
vn 0.341451 -0.862865 -0.372658
vn 0.645664 -0.666516 -0.372658
vn 0.851580 -0.368696 -0.372658
vn 0.725001 -0.027230 -0.688209
vn 0.680234 0.252289 -0.688209
vn 0.531907 0.493399 -0.688209
vn 0.302603 0.659393 -0.688209
vn 0.027230 0.725001 -0.688209
vn -0.252289 0.680234 -0.688209
vn -0.493399 0.531907 -0.688209
vn -0.659393 0.302603 -0.688209
vn -0.725001 0.027230 -0.688209
vn -0.680234 -0.252289 -0.688209
vn -0.531907 -0.493399 -0.688209
vn -0.302603 -0.659393 -0.688209
vn -0.027230 -0.725001 -0.688209
vn 0.252289 -0.680234 -0.688209
vn 0.493399 -0.531907 -0.688209
vn 0.659393 -0.302603 -0.688209
vn 0.439605 -0.035510 -0.897489
vn 0.419732 0.135423 -0.897489
vn 0.335958 0.285739 -0.897489
vn 0.201037 0.392553 -0.897489
vn 0.035510 0.439605 -0.897489
vn -0.135423 0.419732 -0.897489
vn -0.285739 0.335958 -0.897489
vn -0.392553 0.201037 -0.897489
vn -0.439605 0.035510 -0.897489
vn -0.419732 -0.135423 -0.897489
vn -0.335958 -0.285739 -0.897489
vn -0.201037 -0.392553 -0.897489
vn -0.035510 -0.439605 -0.897489
vn 0.135423 -0.419732 -0.897489
vn 0.285739 -0.335958 -0.897489
vn 0.392553 -0.201037 -0.897489
vn 0.000000 -0.000000 -1.000000
usemtl Red
f 1/1/1 2/1/2 3/1/3
f 1/1/1 3/1/3 4/1/4
f 1/1/1 4/1/4 5/1/5
f 1/1/1 5/1/5 6/1/6
f 1/1/1 6/1/6 7/1/7
f 1/1/1 7/1/7 8/1/8
f 1/1/1 8/1/8 9/1/9
f 1/1/1 9/1/9 10/1/10
f 1/1/1 10/1/10 11/1/11
f 1/1/1 11/1/11 12/1/12
f 1/1/1 12/1/12 13/1/13
f 1/1/1 13/1/13 14/1/14
f 1/1/1 14/1/14 15/1/15
f 1/1/1 15/1/15 16/1/16
f 1/1/1 16/1/16 17/1/17
f 1/1/1 17/1/17 2/1/2
f 2/1/2 18/1/18 19/1/19
f 2/1/2 19/1/19 3/1/3
f 3/1/3 19/1/19 20/1/20
f 3/1/3 20/1/20 4/1/4
f 4/1/4 20/1/20 21/1/21
f 4/1/4 21/1/21 5/1/5
f 5/1/5 21/1/21 22/1/22
f 5/1/5 22/1/22 6/1/6
f 6/1/6 22/1/22 23/1/23
f 6/1/6 23/1/23 7/1/7
f 7/1/7 23/1/23 24/1/24
f 7/1/7 24/1/24 8/1/8
f 8/1/8 24/1/24 25/1/25
f 8/1/8 25/1/25 9/1/9
f 9/1/9 25/1/25 26/1/26
f 9/1/9 26/1/26 10/1/10
f 10/1/10 26/1/26 27/1/27
f 10/1/10 27/1/27 11/1/11
f 11/1/11 27/1/27 28/1/28
f 11/1/11 28/1/28 12/1/12
f 12/1/12 28/1/28 29/1/29
f 12/1/12 29/1/29 13/1/13
f 13/1/13 29/1/29 30/1/30
f 13/1/13 30/1/30 14/1/14
f 14/1/14 30/1/30 31/1/31
f 14/1/14 31/1/31 15/1/15
f 15/1/15 31/1/31 32/1/32
f 15/1/15 32/1/32 16/1/16
f 16/1/16 32/1/32 33/1/33
f 16/1/16 33/1/33 17/1/17
f 17/1/17 33/1/33 18/1/18
f 17/1/17 18/1/18 2/1/2
f 18/1/18 34/1/34 35/1/35
f 18/1/18 35/1/35 19/1/19
f 19/1/19 35/1/35 36/1/36
f 19/1/19 36/1/36 20/1/20
f 20/1/20 36/1/36 37/1/37
f 20/1/20 37/1/37 21/1/21
f 21/1/21 37/1/37 38/1/38
f 21/1/21 38/1/38 22/1/22
f 22/1/22 38/1/38 39/1/39
f 22/1/22 39/1/39 23/1/23
f 23/1/23 39/1/39 40/1/40
f 23/1/23 40/1/40 24/1/24
f 24/1/24 40/1/40 41/1/41
f 24/1/24 41/1/41 25/1/25
f 25/1/25 41/1/41 42/1/42
f 25/1/25 42/1/42 26/1/26
f 26/1/26 42/1/42 43/1/43
f 26/1/26 43/1/43 27/1/27
f 27/1/27 43/1/43 44/1/44
f 27/1/27 44/1/44 28/1/28
f 28/1/28 44/1/44 45/1/45
f 28/1/28 45/1/45 29/1/29
f 29/1/29 45/1/45 46/1/46
f 29/1/29 46/1/46 30/1/30
f 30/1/30 46/1/46 47/1/47
f 30/1/30 47/1/47 31/1/31
f 31/1/31 47/1/47 48/1/48
f 31/1/31 48/1/48 32/1/32
f 32/1/32 48/1/48 49/1/49
f 32/1/32 49/1/49 33/1/33
f 33/1/33 49/1/49 34/1/34
f 33/1/33 34/1/34 18/1/18
f 34/1/34 50/1/50 51/1/51
f 34/1/34 51/1/51 35/1/35
f 35/1/35 51/1/51 52/1/52
f 35/1/35 52/1/52 36/1/36
f 36/1/36 52/1/52 53/1/53
f 36/1/36 53/1/53 37/1/37
f 37/1/37 53/1/53 54/1/54
f 37/1/37 54/1/54 38/1/38
f 38/1/38 54/1/54 55/1/55
f 38/1/38 55/1/55 39/1/39
f 39/1/39 55/1/55 56/1/56
f 39/1/39 56/1/56 40/1/40
f 40/1/40 56/1/56 57/1/57
f 40/1/40 57/1/57 41/1/41
f 41/1/41 57/1/57 58/1/58
f 41/1/41 58/1/58 42/1/42
f 42/1/42 58/1/58 59/1/59
f 42/1/42 59/1/59 43/1/43
f 43/1/43 59/1/59 60/1/60
f 43/1/43 60/1/60 44/1/44
f 44/1/44 60/1/60 61/1/61
f 44/1/44 61/1/61 45/1/45
f 45/1/45 61/1/61 62/1/62
f 45/1/45 62/1/62 46/1/46
f 46/1/46 62/1/62 63/1/63
f 46/1/46 63/1/63 47/1/47
f 47/1/47 63/1/63 64/1/64
f 47/1/47 64/1/64 48/1/48
f 48/1/48 64/1/64 65/1/65
f 48/1/48 65/1/65 49/1/49
f 49/1/49 65/1/65 50/1/50
f 49/1/49 50/1/50 34/1/34
usemtl Blue
f 50/2/50 66/2/66 67/2/67
f 50/2/50 67/2/67 51/2/51
f 51/2/51 67/2/67 68/2/68
f 51/2/51 68/2/68 52/2/52
f 52/2/52 68/2/68 69/2/69
f 52/2/52 69/2/69 53/2/53
f 53/2/53 69/2/69 70/2/70
f 53/2/53 70/2/70 54/2/54
f 54/2/54 70/2/70 71/2/71
f 54/2/54 71/2/71 55/2/55
f 55/2/55 71/2/71 72/2/72
f 55/2/55 72/2/72 56/2/56
f 56/2/56 72/2/72 73/2/73
f 56/2/56 73/2/73 57/2/57
f 57/2/57 73/2/73 74/2/74
f 57/2/57 74/2/74 58/2/58
f 58/2/58 74/2/74 75/2/75
f 58/2/58 75/2/75 59/2/59
f 59/2/59 75/2/75 76/2/76
f 59/2/59 76/2/76 60/2/60
f 60/2/60 76/2/76 77/2/77
f 60/2/60 77/2/77 61/2/61
f 61/2/61 77/2/77 78/2/78
f 61/2/61 78/2/78 62/2/62
f 62/2/62 78/2/78 79/2/79
f 62/2/62 79/2/79 63/2/63
f 63/2/63 79/2/79 80/2/80
f 63/2/63 80/2/80 64/2/64
f 64/2/64 80/2/80 81/2/81
f 64/2/64 81/2/81 65/2/65
f 65/2/65 81/2/81 66/2/66
f 65/2/65 66/2/66 50/2/50
f 66/2/66 82/2/82 83/2/83
f 66/2/66 83/2/83 67/2/67
f 67/2/67 83/2/83 84/2/84
f 67/2/67 84/2/84 68/2/68
f 68/2/68 84/2/84 85/2/85
f 68/2/68 85/2/85 69/2/69
f 69/2/69 85/2/85 86/2/86
f 69/2/69 86/2/86 70/2/70
f 70/2/70 86/2/86 87/2/87
f 70/2/70 87/2/87 71/2/71
f 71/2/71 87/2/87 88/2/88
f 71/2/71 88/2/88 72/2/72
f 72/2/72 88/2/88 89/2/89
f 72/2/72 89/2/89 73/2/73
f 73/2/73 89/2/89 90/2/90
f 73/2/73 90/2/90 74/2/74
f 74/2/74 90/2/90 91/2/91
f 74/2/74 91/2/91 75/2/75
f 75/2/75 91/2/91 92/2/92
f 75/2/75 92/2/92 76/2/76
f 76/2/76 92/2/92 93/2/93
f 76/2/76 93/2/93 77/2/77
f 77/2/77 93/2/93 94/2/94
f 77/2/77 94/2/94 78/2/78
f 78/2/78 94/2/94 95/2/95
f 78/2/78 95/2/95 79/2/79
f 79/2/79 95/2/95 96/2/96
f 79/2/79 96/2/96 80/2/80
f 80/2/80 96/2/96 97/2/97
f 80/2/80 97/2/97 81/2/81
f 81/2/81 97/2/97 82/2/82
f 81/2/81 82/2/82 66/2/66
f 82/2/82 98/2/98 99/2/99
f 82/2/82 99/2/99 83/2/83
f 83/2/83 99/2/99 100/2/100
f 83/2/83 100/2/100 84/2/84
f 84/2/84 100/2/100 101/2/101
f 84/2/84 101/2/101 85/2/85
f 85/2/85 101/2/101 102/2/102
f 85/2/85 102/2/102 86/2/86
f 86/2/86 102/2/102 103/2/103
f 86/2/86 103/2/103 87/2/87
f 87/2/87 103/2/103 104/2/104
f 87/2/87 104/2/104 88/2/88
f 88/2/88 104/2/104 105/2/105
f 88/2/88 105/2/105 89/2/89
f 89/2/89 105/2/105 106/2/106
f 89/2/89 106/2/106 90/2/90
f 90/2/90 106/2/106 107/2/107
f 90/2/90 107/2/107 91/2/91
f 91/2/91 107/2/107 108/2/108
f 91/2/91 108/2/108 92/2/92
f 92/2/92 108/2/108 109/2/109
f 92/2/92 109/2/109 93/2/93
f 93/2/93 109/2/109 110/2/110
f 93/2/93 110/2/110 94/2/94
f 94/2/94 110/2/110 111/2/111
f 94/2/94 111/2/111 95/2/95
f 95/2/95 111/2/111 112/2/112
f 95/2/95 112/2/112 96/2/96
f 96/2/96 112/2/112 113/2/113
f 96/2/96 113/2/113 97/2/97
f 97/2/97 113/2/113 98/2/98
f 97/2/97 98/2/98 82/2/82
f 114/2/114 99/2/99 98/2/98
f 114/2/114 100/2/100 99/2/99
f 114/2/114 101/2/101 100/2/100
f 114/2/114 102/2/102 101/2/101
f 114/2/114 103/2/103 102/2/102
f 114/2/114 104/2/104 103/2/103
f 114/2/114 105/2/105 104/2/104
f 114/2/114 106/2/106 105/2/105
f 114/2/114 107/2/107 106/2/106
f 114/2/114 108/2/108 107/2/107
f 114/2/114 109/2/109 108/2/108
f 114/2/114 110/2/110 109/2/109
f 114/2/114 111/2/111 110/2/110
f 114/2/114 112/2/112 111/2/111
f 114/2/114 113/2/113 112/2/112
f 114/2/114 98/2/98 113/2/113
Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

+98 -34
View File
@@ -37,7 +37,7 @@
#include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp" #include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Fill/FillGyroid.hpp" #include "libslic3r/Fill/FillGyroid.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp" #include "libslic3r/Fill/AdaptiveInfillField.hpp"
#include "libslic3r/Flow.hpp" #include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp" #include "libslic3r/IntersectionPoints.hpp"
@@ -1719,9 +1719,10 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "
const SparseInfillShape smooth = shape_for("100%"); const SparseInfillShape smooth = shape_for("100%");
REQUIRE(sharp.path_count > 0); REQUIRE(sharp.path_count > 0);
// Smoothing should leave the loop count about where it was; uncapping the smoothing reach, the // Smoothing should leave the path count about where it was; uncapping the smoothing reach, the
// regression this guards against, adds about 10%. // regression this guards against, adds a quarter to a third. Outline ends next to each other are
const size_t allowed_extra = sharp.path_count / 50; // 2% // joined into one path, so a few loops more or less are a larger share of the count.
const size_t allowed_extra = sharp.path_count / 10; // 10%
REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra); REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra);
// The outlines are still rounded. // The outlines are still rounded.
REQUIRE(smooth.point_count > sharp.point_count); REQUIRE(smooth.point_count > sharp.point_count);
@@ -2060,9 +2061,9 @@ TEST_CASE("Adaptive TPMS infill thins out from the surface to the interior densi
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(60)}, print, Slic3r::Test::init_and_process_print({Slic3r::Test::cube(60)}, print,
{{"sparse_infill_pattern", pattern}, {{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "25%"}, {"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive}, {"adaptive_infill_mode", adaptive},
{"tpms_interior_density", "5%"}, {"adaptive_infill_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"}, {"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4}, {"layer_height", 0.4},
{"initial_layer_print_height", 0.4}}); {"initial_layer_print_height", 0.4}});
}; };
@@ -2089,9 +2090,9 @@ TEST_CASE("Adaptive TPMS infill of a tall object is sparsest at its middle heigh
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print, Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "tpmsd"}, {{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"}, {"sparse_infill_density", "25%"},
{"tpms_adaptive", "lobes"}, {"adaptive_infill_mode", "lobes"},
{"tpms_interior_density", "5%"}, {"adaptive_infill_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"}, {"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4}, {"layer_height", 0.4},
{"initial_layer_print_height", 0.4}}); {"initial_layer_print_height", 0.4}});
const Polygons core = centered_square(5.); const Polygons core = centered_square(5.);
@@ -2111,9 +2112,9 @@ TEST_CASE("2D adaptive TPMS infill does not change along its axis", "[Fill]")
Slic3r::Test::init_and_process_print({along_x ? make_cube(90., 30., 30.) : make_cube(30., 30., 90.)}, print, Slic3r::Test::init_and_process_print({along_x ? make_cube(90., 30., 30.) : make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "gyroid"}, {{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "25%"}, {"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive}, {"adaptive_infill_mode", adaptive},
{"tpms_interior_density", "5%"}, {"adaptive_infill_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"}, {"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4}, {"layer_height", 0.4},
{"initial_layer_print_height", 0.4}}); {"initial_layer_print_height", 0.4}});
}; };
@@ -2147,9 +2148,9 @@ TEST_CASE("Adaptive TPMS shells and blend keep the whole deep core of a tall obj
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print, Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "tpmsd"}, {{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"}, {"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive}, {"adaptive_infill_mode", adaptive},
{"tpms_interior_density", "5%"}, {"adaptive_infill_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"}, {"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4}, {"layer_height", 0.4},
{"initial_layer_print_height", 0.4}}); {"initial_layer_print_height", 0.4}});
}; };
@@ -2179,9 +2180,9 @@ TEST_CASE("Adaptive TPMS infill is sparse at the center of both of two united sp
Slic3r::Test::init_and_process_print({TriangleMesh(spheres)}, print, Slic3r::Test::init_and_process_print({TriangleMesh(spheres)}, print,
{{"sparse_infill_pattern", "gyroid"}, {{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "25%"}, {"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive}, {"adaptive_infill_mode", adaptive},
{"tpms_interior_density", "5%"}, {"adaptive_infill_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"}, {"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4}, {"layer_height", 0.4},
{"initial_layer_print_height", 0.4}}); {"initial_layer_print_height", 0.4}});
}; };
@@ -2209,9 +2210,9 @@ TEST_CASE("Adaptive TPMS gradients keep the surface density deeper in the order
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(40)}, print, Slic3r::Test::init_and_process_print({Slic3r::Test::cube(40)}, print,
{{"sparse_infill_pattern", "tpmsd"}, {{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"}, {"sparse_infill_density", "25%"},
{"tpms_adaptive", "lobes"}, {"adaptive_infill_mode", "lobes"},
{"tpms_interior_density", "5%"}, {"adaptive_infill_interior_density", "5%"},
{"tpms_adaptive_gradient", gradient}, {"adaptive_infill_gradient", gradient},
{"layer_height", 0.4}, {"layer_height", 0.4},
{"initial_layer_print_height", 0.4}}); {"initial_layer_print_height", 0.4}});
return sparse_infill_length(print, 0., 40., centered_square(20.)); return sparse_infill_length(print, 0., 40., centered_square(20.));
@@ -2236,9 +2237,9 @@ TEST_CASE("Adaptive TPMS settings leave the infill unchanged when they do not ap
{{"sparse_infill_pattern", pattern}, {{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "20%"}, {"sparse_infill_density", "20%"},
{"layer_height", 0.2}, {"layer_height", 0.2},
{"tpms_adaptive", adaptive}, {"adaptive_infill_mode", adaptive},
{"tpms_interior_density", "40%"}, {"adaptive_infill_interior_density", "40%"},
{"tpms_adaptive_gradient", "exponential"}}); {"adaptive_infill_gradient", "exponential"}});
const SparseInfillShape expected = sparse_infill_shape(reference); const SparseInfillShape expected = sparse_infill_shape(reference);
REQUIRE(expected.path_count > 0); REQUIRE(expected.path_count > 0);
CHECK(sparse_infill_shape(tuned).sequence == expected.sequence); CHECK(sparse_infill_shape(tuned).sequence == expected.sequence);
@@ -2252,7 +2253,7 @@ TEST_CASE("Adaptive gyroid infill ignores the Z-buckling optimization", "[Fill]"
{{"sparse_infill_pattern", "gyroid"}, {{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "10%"}, {"sparse_infill_density", "10%"},
{"gyroid_optimized", gyroid_optimized}, {"gyroid_optimized", gyroid_optimized},
{"tpms_adaptive", "lobes"}, {"adaptive_infill_mode", "lobes"},
{"layer_height", 0.2}}); {"layer_height", 0.2}});
return sparse_infill_shape(print); return sparse_infill_shape(print);
}; };
@@ -2268,10 +2269,10 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
// An 80 x 80 x 40 mm box around both regions, which share its radial field. // An 80 x 80 x 40 mm box around both regions, which share its radial field.
const ExPolygons box{ExPolygon(Points{Point::new_scale(60., 20.), Point::new_scale(140., 20.), Point::new_scale(140., 100.), const ExPolygons box{ExPolygon(Points{Point::new_scale(60., 20.), Point::new_scale(140., 20.), Point::new_scale(140., 100.),
Point::new_scale(60., 100.)})}; Point::new_scale(60., 100.)})};
std::vector<TpmsRadialField::Slice> slices; std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 200; ++i) for (int i = 0; i < 200; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &box}); slices.push_back({0.2 * i, 0.2 * (i + 1), &box});
const TpmsRadialField field(slices, get_extents(box), TpmsAdaptiveMode::Lobes, [] {}); const AdaptiveInfillField field(slices, get_extents(box), AdaptiveInfillMode::Lobes, [] {});
auto circle = [](double radius) { auto circle = [](double radius) {
Polygon contour = make_circle_num_segments(scale_(radius), 120); Polygon contour = make_circle_num_segments(scale_(radius), 120);
@@ -2285,13 +2286,13 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
filler->spacing = 0.45; filler->spacing = 0.45;
filler->angle = float(M_PI / 7.); filler->angle = float(M_PI / 7.);
filler->z = z; filler->z = z;
filler->tpms_radial_field = &field; filler->adaptive_infill_field = &field;
FillParams params; FillParams params;
params.density = 0.2f; params.density = 0.2f;
params.tpms_adaptive = TpmsAdaptiveMode::Lobes; params.adaptive_infill_mode = AdaptiveInfillMode::Lobes;
params.tpms_interior_density = 0.05f; params.adaptive_infill_interior_density = 0.05f;
params.tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear; params.adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
params.layer_height = 0.2; params.layer_height = 0.2;
params.dont_adjust = true; params.dont_adjust = true;
Surface surface(stInternal, expolygon); Surface surface(stInternal, expolygon);
@@ -2328,8 +2329,8 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(30)}, print, Slic3r::Test::init_and_process_print({Slic3r::Test::cube(30)}, print,
{{"sparse_infill_pattern", pattern}, {{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "25%"}, {"sparse_infill_density", "25%"},
{"tpms_adaptive", "lobes"}, {"adaptive_infill_mode", "lobes"},
{"tpms_interior_density", "5%"}, {"adaptive_infill_interior_density", "5%"},
{"layer_height", 0.2}, {"layer_height", 0.2},
{"initial_layer_print_height", 0.2}, {"initial_layer_print_height", 0.2},
{"resolution", 0.012}}); {"resolution", 0.012}});
@@ -2353,3 +2354,66 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
max_distance = std::max(max_distance, printed_tree.distance_from_lines<false>(point)); max_distance = std::max(max_distance, printed_tree.distance_from_lines<false>(point));
CHECK(unscale<double>(max_distance) <= 0.012); CHECK(unscale<double>(max_distance) <= 0.012);
} }
TEST_CASE("Infill lines whose ends are next to each other join into one path", "[Fill]")
{
// An out-and-back pair 1 mm apart, as multiline infill leaves, and a line 19 mm away.
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
Polyline(Point::new_scale(0., 20.), Point::new_scale(10., 20.)) };
join_close_ends(lines, scale_(1.5), true);
REQUIRE(lines.size() == 2);
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
REQUIRE(joined != lines.end());
// Both lines plus the 1 mm between them, joined at one end only, as joining the other end too would close a loop.
CHECK_THAT(unscale<double>(joined->length()), Catch::Matchers::WithinAbs(21., 1e-6));
CHECK(joined->first_point() != joined->last_point());
}
TEST_CASE("Joined infill lines print each line once", "[Fill]")
{
// The middle line comes first and both of its ends get joined, so the group has to be walked from an outer line.
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
Polyline(Point::new_scale(10., -1.), Point::new_scale(0., -1.)) };
join_close_ends(lines, scale_(1.5), true);
REQUIRE(lines.size() == 1);
CHECK(lines.front().size() == 6);
// Three 10 mm lines and two 1 mm joins.
CHECK_THAT(unscale<double>(lines.front().length()), Catch::Matchers::WithinAbs(32., 1e-6));
}
TEST_CASE("Infill line ends are not joined across another line", "[Fill]")
{
// A pair 1 mm apart whose left ends have a third line running between them, which a join there would cross.
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
Polyline(Point::new_scale(-5., 0.5), Point::new_scale(5., 0.5)) };
join_close_ends(lines, scale_(1.5), true);
REQUIRE(lines.size() == 2);
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
REQUIRE(joined != lines.end());
// Joined on the right, where nothing is in between, so the left ends stay open.
CHECK(joined->first_point().x() == 0);
CHECK(joined->last_point().x() == 0);
}
TEST_CASE("Infill lines that cannot reverse join only end to start", "[Fill]")
{
const Point a(Point::new_scale(0., 0.)), b(Point::new_scale(10., 0.)), c(Point::new_scale(10., 1.)), d(Point::new_scale(0., 1.));
SECTION("Ends that would need a reversal stay apart") {
Polylines lines { Polyline(a, b), Polyline(d, c) };
join_close_ends(lines, scale_(1.5), false);
CHECK(lines.size() == 2);
}
SECTION("The end of one line joins the start of the next, both kept in their direction") {
Polylines lines { Polyline(c, d), Polyline(a, b) };
join_close_ends(lines, scale_(1.5), false);
REQUIRE(lines.size() == 1);
const Points &points = lines.front().points;
REQUIRE(points.size() == 4);
auto position = [&points](const Point &p) { return std::find(points.begin(), points.end(), p) - points.begin(); };
CHECK(position(a) < position(b));
CHECK(position(c) < position(d));
}
}
+200
View File
@@ -2,15 +2,18 @@
#include <catch2/matchers/catch_matchers_floating_point.hpp> #include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp> #include <catch2/matchers/catch_matchers.hpp>
#include <catch2/catch_message.hpp> #include <catch2/catch_message.hpp>
#include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/MultiNozzleUtils.hpp" #include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -30,6 +33,7 @@ namespace Slic3r { enum ExtrusionRole : uint8_t; }
using namespace Slic3r; using namespace Slic3r;
using Catch::Matchers::WithinAbs; using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
// Regression coverage for filament/tool-change time being folded into the first // Regression coverage for filament/tool-change time being folded into the first
// pending motion block (an extrusion move) instead of the tool-change move, and // pending motion block (an extrusion move) instead of the tool-change move, and
@@ -570,6 +574,33 @@ double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double j
} // namespace } // namespace
TEST_CASE("Klipper extrusion ratio changes use its own corner velocity, not E jerk", "[GCodeTiming][JunctionDeviation]")
{
const double jerk_e = GENERATE(0.0, 100.0);
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_jerk_e.values = {jerk_e, jerk_e};
GCodeProcessor processor;
run_processor(processor, config,
"G90\nM83\nSET_VELOCITY_LIMIT ACCEL=1000 MINIMUM_CRUISE_RATIO=0\n"
"G1 X20 Y60 F6000\nM400\n"
"G1 X60 Y60 E0.8 F6000\nG1 X100 Y60 E1.6 F6000\nM400\n");
// A 0.02 -> 0.04 extrusion ratio change is capped at 1 / 0.02 = 50 mm/s.
REQUIRE_THAT(corner_speed(processor.get_result()), Catch::Matchers::WithinAbs(50.0, 0.01));
}
TEST_CASE("Klipper legacy smoothing stays absolute across acceleration changes", "[GCodeTiming][Klipper]")
{
const bool modern = GENERATE(false, true);
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
const std::string gcode = std::string("G90\nSET_VELOCITY_LIMIT ACCEL=1000 ") +
(modern ? "MINIMUM_CRUISE_RATIO=0.75\n" : "ACCEL_TO_DECEL=250\n") +
"G1 X40 F30000\nM400\nM204 S2000\nG1 X0\nM400\n";
GCodeProcessor processor;
run_processor(processor, config, gcode.c_str());
const double expected = modern ? 0.5 + 0.25 * std::sqrt(2.0) : 0.95;
REQUIRE_THAT(processor.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinAbs(expected, 1e-5));
}
TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity", TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity",
"[GCodeTiming][JunctionDeviation]") "[GCodeTiming][JunctionDeviation]")
{ {
@@ -669,3 +700,172 @@ TEST_CASE("How fast a corner is taken does not depend on how much is extruded th
Catch::Matchers::WithinRel(marlin, 0.02)); Catch::Matchers::WithinRel(marlin, 0.02));
} }
} }
// Expected times are Klipper's own planner math (klippy/toolhead.py). For a move from rest to rest,
// LookAheadQueue.flush() reduces to cruise_v2 = min(vmax^2, d * accel, d * pseudo_accel) with
// pseudo_accel = accel * (1 - minimum_cruise_ratio), and Move.set_junction() to a trapezoid at accel;
// a move too short to reach its speed still spends that ratio of its length cruising.
namespace {
constexpr double klipper_accel = 1000.0; // make_junction_config's acceleration
constexpr double klipper_cruise_ratio = 0.5;
constexpr double klipper_pseudo_accel = klipper_accel * (1.0 - klipper_cruise_ratio);
// Time of a move that starts and ends at rest.
double rest_to_rest_time(double distance, double max_speed, double accel, double pseudo_accel)
{
const double cruise_v2 = std::min(sqr(max_speed), distance * pseudo_accel);
const double cruise = std::sqrt(cruise_v2);
return 2.0 * cruise / accel + (distance - cruise_v2 / accel) / cruise;
}
// Every run states the cruise ratio rather than relying on the estimator's built-in default.
void run_klipper(GCodeProcessor& proc, const std::string& gcode,
const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
{
const std::string limits = "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) + "\n";
run_processor(proc, config, (limits + gcode).c_str());
}
double klipper_time(const std::string& gcode, const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
{
GCodeProcessor proc;
run_klipper(proc, gcode, config);
return proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
}
double time_after_retract(const GCodeProcessorResult& r)
{
double t = 0.0;
bool after = false;
for (const auto& mv : r.moves) {
if (mv.type == EMoveType::Retract)
after = true;
else if (after)
t += mv.time[NORMAL];
}
return t;
}
} // namespace
TEST_CASE("A straight line split into short segments is planned as one move", "[GCodeTiming][Klipper]")
{
// 100 mm at 50 mm/s queues 2 s of motion, so the look-ahead flushes lazily part-way along the
// line; at 10 mm/s^2 the ramps span many segments and the cruise ratio caps the speed.
const int segments = GENERATE(1, 100, 1000);
std::ostringstream gcode;
gcode << "SET_VELOCITY_LIMIT ACCEL=10\n" << std::fixed << std::setprecision(3);
for (int i = 1; i <= segments; ++i)
gcode << "G1 X" << 100.0 * i / segments << " F3000\n";
REQUIRE_THAT(klipper_time(gcode.str()),
WithinRel(rest_to_rest_time(100.0, 50.0, 10.0, 10.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
}
TEST_CASE("Where the look-ahead flushes does not change the estimate", "[GCodeTiming][Klipper]")
{
// A retraction stops the toolhead, so the zig-zag after it is planned on its own; the straight run
// before it only shifts where the queue flushes lazily (1 s of motion after a stop, then every 0.15 s).
auto zigzag_time = [](int run_moves) {
std::string gcode = "M83\nG91\n";
for (int i = 0; i < run_moves; ++i)
gcode += "G1 X1 F6000\n";
gcode += "G1 E-1 F2400\n";
const char* moves[] = {"G1 X2 F3000\n", "G1 Y2 F9000\n", "G1 X2 F1200\n", "G1 Y-2 F9000\n"};
for (int i = 0; i < 24; ++i)
gcode += moves[i % 4];
GCodeProcessor proc;
run_klipper(proc, gcode);
return time_after_retract(proc.get_result());
};
const double reference = zigzag_time(0);
REQUIRE(reference > 0.0);
for (int run_moves : {30, 80, 105, 130}) {
INFO("run moves = " << run_moves);
REQUIRE_THAT(zigzag_time(run_moves), WithinAbs(reference, 1e-9));
}
}
TEST_CASE("A short move into a corner is limited by the centripetal term, not the corner velocity",
"[GCodeTiming][Klipper]")
{
// A 50 mm/s square corner velocity allows 50 mm/s through the right angle; the 1 mm move into it
// allows sqrt(0.5 * 1 * 1000 * tan(45 deg)) = 22.4 mm/s (Move.calc_junction's move_centripetal_v2).
GCodeProcessor proc;
run_klipper(proc, "G90\nG1 X59 Y60 F6000\nM400\nG1 X60 Y60 F9000\nG1 X60 Y100 F9000\n",
make_junction_config(gcfKlipper, 50.0, 0.0));
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(std::sqrt(500.0), 1e-6));
}
TEST_CASE("A corner keeps the square corner velocity when the acceleration changes between its moves",
"[GCodeTiming][Klipper]")
{
GCodeProcessor proc;
run_klipper(proc, "G90\nG1 X20 Y60 F6000\nM400\nG1 X60 Y60 F9000\nM204 S4000\nG1 X60 Y100 F9000\n");
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(5.0, 1e-6));
}
TEST_CASE("A retraction stops the toolhead and runs at the extruder's own limits", "[GCodeTiming][Klipper]")
{
// An E acceleration of 0 is unset, and the retraction then uses the retract acceleration.
const auto [e_accel, retract_accel] = GENERATE(table<double, double>({{20000.0, 20000.0}, {0.0, 3000.0}}));
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_acceleration_e.values = {e_accel, e_accel};
config.machine_max_acceleration_retracting.values = {3000.0, 3000.0};
GCodeProcessor proc;
run_klipper(proc, "G90\nM83\nG1 X60 Y60 F9000\nG1 E-1 F2400\nG1 X100 Y60 F9000\n", config);
REQUIRE_THAT(corner_speed(proc.get_result()), WithinAbs(0.0, 1e-6));
const double expected = rest_to_rest_time(std::sqrt(2.0) * 60.0, 150.0, klipper_accel, klipper_pseudo_accel) +
rest_to_rest_time(1.0, 40.0, retract_accel, std::min(retract_accel, klipper_pseudo_accel)) +
rest_to_rest_time(40.0, 150.0, klipper_accel, klipper_pseudo_accel);
REQUIRE_THAT(proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinRel(expected, 1e-6));
}
TEST_CASE("Extruder limits apply to retractions, not to extrusion along a path", "[GCodeTiming][Klipper]")
{
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_speed_e.values = {10.0, 10.0};
config.machine_max_acceleration_e.values = {10.0, 10.0};
REQUIRE_THAT(klipper_time("M83\nG1 X40 E20 F30000\n", config),
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
REQUIRE_THAT(klipper_time("M83\nG1 E-1 F2400\n", config),
WithinRel(rest_to_rest_time(1.0, 10.0, 10.0, 10.0), 1e-6));
}
TEST_CASE("A Z move is limited by the Z axis speed and acceleration", "[GCodeTiming][Klipper]")
{
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_speed_z.values = {20.0, 20.0};
config.machine_max_acceleration_z.values = {100.0, 100.0};
REQUIRE_THAT(klipper_time("G1 Z10 F1200\n", config),
WithinRel(rest_to_rest_time(10.0, 20.0, 100.0, 100.0), 1e-6));
}
TEST_CASE("M204 sets the acceleration from S, or the smaller of P and T", "[GCodeTiming][Klipper]")
{
const char* m204 = GENERATE("M204 S500", "M204 P500 T2000", "M204 P2000 T500");
REQUIRE_THAT(klipper_time(std::string(m204) + "\nG1 X100 F30000\n"),
WithinRel(rest_to_rest_time(100.0, 500.0, 500.0, 500.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
}
TEST_CASE("SET_VELOCITY_LIMIT applies every limit on the line and ignores the comment", "[GCodeTiming][Klipper]")
{
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT VELOCITY=80 ACCEL=500 MINIMUM_CRUISE_RATIO=0.2 ; ACCEL=9999\n"
"G1 X20 F30000\n"),
WithinRel(rest_to_rest_time(20.0, 80.0, 500.0, 500.0 * (1.0 - 0.2)), 1e-6));
}
TEST_CASE("ACCEL_TO_DECEL is ignored on a line that also sets the minimum cruise ratio", "[GCodeTiming][Klipper]")
{
// Klipper 0.13 prefers the cruise ratio and later releases no longer read ACCEL_TO_DECEL at all.
// ACCEL_TO_DECEL comes last, so applying the parameters in line order would let it win.
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) +
" ACCEL_TO_DECEL=100\nG1 X40 F30000\n"),
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
}
TEST_CASE("M400 without a delay waits for the queue to drain", "[GCodeTiming][Klipper]")
{
REQUIRE_THAT(klipper_time("G90\nG1 X40 F30000\nM400\nG1 X80\n"),
WithinRel(2.0 * rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
}
+36 -1
View File
@@ -514,7 +514,7 @@ TEST_CASE("Acceleration and velocity limit commands print their values in genera
const Case c = GENERATE(values<Case>({ const Case c = GENERATE(values<Case>({
{gcfKlipper, Command::KlipperLimits, 2000000, 25. / 3., false, {gcfKlipper, Command::KlipperLimits, 2000000, 25. / 3., false,
{"SET_VELOCITY_LIMIT ACCEL=2000000 ", "ACCEL_TO_DECEL=1e+06 ", "SQUARE_CORNER_VELOCITY=8.33333\n"}, {}}, {"SET_VELOCITY_LIMIT ACCEL=2000000 ", "ACCEL_TO_DECEL=1e+06 ", "SQUARE_CORNER_VELOCITY=8.33333\n"}, {}},
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5\n"}, {"SQUARE_CORNER_VELOCITY"}}, {gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5 ", "MINIMUM_CRUISE_RATIO=0.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
{gcfKlipper, Command::KlipperLimits, 0, 0.25, true, {"SQUARE_CORNER_VELOCITY=0.25 ", "; adjust VELOCITY_LIMIT"}, {"ACCEL"}}, {gcfKlipper, Command::KlipperLimits, 0, 0.25, true, {"SQUARE_CORNER_VELOCITY=0.25 ", "; adjust VELOCITY_LIMIT"}, {"ACCEL"}},
{gcfKlipper, Command::Print, 3001, 0., true, {"ACCEL=3001 ", "ACCEL_TO_DECEL=1500.5 ", "; adjust ACCEL_TO_DECEL", "; adjust acceleration"}, {}}, {gcfKlipper, Command::Print, 3001, 0., true, {"ACCEL=3001 ", "ACCEL_TO_DECEL=1500.5 ", "; adjust ACCEL_TO_DECEL", "; adjust acceleration"}, {}},
{gcfMarlinFirmware, Command::Print, 2500, 0., false, {"M204 P2500\n"}, {}}, {gcfMarlinFirmware, Command::Print, 2500, 0., false, {"M204 P2500\n"}, {}},
@@ -1272,6 +1272,41 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G
REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos); REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos);
} }
TEST_CASE("Klipper acceleration commands send accel_to_decel both as ACCEL_TO_DECEL and as the cruise ratio", "[GCodeWriter]")
{
// Klipper 0.13 defines MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL; older releases read only the
// first parameter and current ones only the second, so both have to describe the same limit.
GCodeWriter writer;
writer.config.gcode_flavor.value = gcfKlipper;
writer.config.accel_to_decel_enable.value = true;
// The writer appends "; adjust ..." comments by default; keep them out so the line ends at the ratio.
struct CommentGuard
{
bool saved = GCodeWriter::full_gcode_comment;
~CommentGuard() { GCodeWriter::full_gcode_comment = saved; }
} comment_guard;
GCodeWriter::full_gcode_comment = false;
SECTION("a 25% factor") {
writer.config.accel_to_decel_factor.value = 25;
REQUIRE_THAT(writer.set_print_acceleration(2000),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=500 MINIMUM_CRUISE_RATIO=0.75"));
REQUIRE_THAT(writer.set_accel_and_jerk(3000, 0.0),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=3000 ACCEL_TO_DECEL=750 MINIMUM_CRUISE_RATIO=0.75"));
}
SECTION("a 100% factor turns the cruise ratio off") {
writer.config.accel_to_decel_factor.value = 100;
REQUIRE_THAT(writer.set_print_acceleration(2000),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=2000 MINIMUM_CRUISE_RATIO=0\n"));
}
SECTION("neither is sent while the option is off") {
writer.config.accel_to_decel_enable.value = false;
const std::string line = writer.set_print_acceleration(2000);
REQUIRE_THAT(line, Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000"));
REQUIRE_THAT(line, !Catch::Matchers::ContainsSubstring("ACCEL_TO_DECEL") && !Catch::Matchers::ContainsSubstring("MINIMUM_CRUISE_RATIO"));
}
}
// Regression test for the belt-printer "illegal gantry move at print start" bug. // Regression test for the belt-printer "illegal gantry move at print start" bug.
// //
// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed // On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed
+17
View File
@@ -202,6 +202,23 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
} }
} }
TEST_CASE("The wipe tower sets Klipper's acceleration and accel_to_decel on one line", "[WipeTower]")
{
// On separate lines, current Klipper finds nothing it reads on the ACCEL_TO_DECEL one and 0.13
// derives the cruise ratio from the acceleration before the change.
DynamicPrintConfig config = wipe_tower_toolchange_config("klipper");
// Type 2 towers leave the acceleration to the main G-code writer.
config.set_deserialize_strict({ { "wipe_tower_type", "type1" },
{ "accel_to_decel_enable", "1" }, { "accel_to_decel_factor", "50%" },
{ "default_acceleration", "1000" }, { "travel_acceleration", "2000" },
{ "machine_max_acceleration_extruding", "5000" } });
const std::string tower = wipe_tower_regions(slice_with_prime_tower(config));
REQUIRE_THAT(tower, Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL="));
CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(" MINIMUM_CRUISE_RATIO=0.5\n"));
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL_TO_DECEL") &&
!Catch::Matchers::ContainsSubstring("M204"));
}
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this: // What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
// they call the estimator directly. The estimate counts the filaments the print really uses, // they call the estimator directly. The estimate counts the filaments the print really uses,
// so the two-filament shape gives the outer wall the second one. // so the two-filament shape gives the outer wall the second one.
+8 -1
View File
@@ -1,5 +1,9 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
# Observe the geometry backend's actual CGAL layout without duplicating its flags.
# This source is only added when the test suite is enabled.
target_sources(libslic3r_cgal PRIVATE texture_import_cgal.cpp)
add_executable(${_TEST_NAME}_tests add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp ${_TEST_NAME}_tests.cpp
test_3mf.cpp test_3mf.cpp
@@ -31,11 +35,12 @@ add_executable(${_TEST_NAME}_tests
test_preset_diff.cpp test_preset_diff.cpp
test_vendor_cache.cpp test_vendor_cache.cpp
test_preset_options.cpp test_preset_options.cpp
test_edge_grid.cpp
test_elephant_foot_compensation.cpp test_elephant_foot_compensation.cpp
test_fill_corner_smoothing.cpp test_fill_corner_smoothing.cpp
test_filament_mixer.cpp test_filament_mixer.cpp
test_fill_plane_path.cpp test_fill_plane_path.cpp
test_fill_tpms_adaptive.cpp test_adaptive_infill_field.cpp
test_geometry.cpp test_geometry.cpp
test_imex_arrange.cpp test_imex_arrange.cpp
test_imex_helpers.cpp test_imex_helpers.cpp
@@ -67,10 +72,12 @@ add_executable(${_TEST_NAME}_tests
test_wipe_path.cpp test_wipe_path.cpp
test_optimizers.cpp test_optimizers.cpp
test_ordering_strategies.cpp test_ordering_strategies.cpp
test_shortest_path.cpp
# test_png_io.cpp # test_png_io.cpp
test_indexed_triangle_set.cpp test_indexed_triangle_set.cpp
test_connected_bodies.cpp test_connected_bodies.cpp
test_texture_displacement.cpp test_texture_displacement.cpp
test_texture_import.cpp
test_instance_lock.cpp test_instance_lock.cpp
../libnest2d/printer_parts.cpp ../libnest2d/printer_parts.cpp
) )
@@ -13,7 +13,7 @@
#include "libslic3r/BoundingBox.hpp" #include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp" #include "libslic3r/Fill/AdaptiveInfillField.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
@@ -30,24 +30,24 @@ ExPolygon rectangle(double x0, double y0, double x1, double y1)
} }
// The expolygons stacked in 0.2 mm layers from z = 0 to height. // The expolygons stacked in 0.2 mm layers from z = 0 to height.
TpmsRadialField radial_field(const ExPolygons &expolygons, double height, TpmsAdaptiveMode mode = TpmsAdaptiveMode::Lobes) AdaptiveInfillField radial_field(const ExPolygons &expolygons, double height, AdaptiveInfillMode mode = AdaptiveInfillMode::Lobes)
{ {
std::vector<TpmsRadialField::Slice> slices; std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i) for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons}); slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons});
return TpmsRadialField(slices, get_extents(expolygons), mode, [] {}); return AdaptiveInfillField(slices, get_extents(expolygons), mode, [] {});
} }
double radial(const TpmsRadialField &field, const Vec3d &pt) double radial(const AdaptiveInfillField &field, const Vec3d &pt)
{ {
TpmsRadialField::Radials radials; AdaptiveInfillField::Radials radials;
field.radial(pt, radials); field.radial(pt, radials);
return radials[0].t; return radials[0].t;
} }
Vec3d center(const TpmsRadialField &field, const Vec3d &pt) Vec3d center(const AdaptiveInfillField &field, const Vec3d &pt)
{ {
TpmsRadialField::Radials radials; AdaptiveInfillField::Radials radials;
field.radial(pt, radials); field.radial(pt, radials);
return radials[0].center; return radials[0].center;
} }
@@ -57,11 +57,11 @@ constexpr double Tolerance = 0.075;
} // namespace } // namespace
TEST_CASE("TPMS radial field is zero at the center of a cube and one at its faces", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field is zero at the center of a cube and one at its faces", "[AdaptiveInfillField]")
{ {
// A 20 mm cube, 10 mm from its center to every face. // A 20 mm cube, 10 mm from its center to every face.
const ExPolygons square{rectangle(0., 0., 20., 20.)}; const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.); const AdaptiveInfillField field = radial_field(square, 20.);
const Vec3d c = center(field, {10., 10., 10.}); const Vec3d c = center(field, {10., 10., 10.});
CHECK_THAT(c.x(), WithinAbs(10., 0.5)); CHECK_THAT(c.x(), WithinAbs(10., 0.5));
@@ -74,10 +74,10 @@ TEST_CASE("TPMS radial field is zero at the center of a cube and one at its face
} }
} }
TEST_CASE("TPMS radial field grows linearly from the center of a cube to its faces", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field grows linearly from the center of a cube to its faces", "[AdaptiveInfillField]")
{ {
const ExPolygons square{rectangle(0., 0., 20., 20.)}; const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.); const AdaptiveInfillField field = radial_field(square, 20.);
for (double d = 1.; d < 10.; d += 1.) { for (double d = 1.; d < 10.; d += 1.) {
CAPTURE(d); CAPTURE(d);
CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance)); CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance));
@@ -85,11 +85,11 @@ TEST_CASE("TPMS radial field grows linearly from the center of a cube to its fac
} }
} }
TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field of a tall box is centered at its middle height", "[AdaptiveInfillField]")
{ {
// 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom. // 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const ExPolygons square{rectangle(0., 0., 20., 20.)}; const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60.); const AdaptiveInfillField field = radial_field(square, 60.);
CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5)); CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5));
CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance)); CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance));
@@ -97,10 +97,10 @@ TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[
CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance)); CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance));
} }
TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field grades every body towards its own center", "[AdaptiveInfillField]")
{ {
const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)}; const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)};
const TpmsRadialField field = radial_field(squares, 20.); const AdaptiveInfillField field = radial_field(squares, 20.);
CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5)); CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5));
CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5)); CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5));
@@ -108,20 +108,20 @@ TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTp
CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance)); CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance));
} }
TEST_CASE("TPMS radial field is beyond one outside of the object", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field is beyond one outside of the object", "[AdaptiveInfillField]")
{ {
const ExPolygons square{rectangle(0., 0., 20., 20.)}; const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.); const AdaptiveInfillField field = radial_field(square, 20.);
CHECK(radial(field, {-5., 10., 10.}) > 1.); CHECK(radial(field, {-5., 10., 10.}) > 1.);
CHECK(radial(field, {10., 10., 30.}) > 1.); CHECK(radial(field, {10., 10., 30.}) > 1.);
} }
TEST_CASE("TPMS radial field grades every lobe of a body towards its own center", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field grades every lobe of a body towards its own center", "[AdaptiveInfillField]")
{ {
// Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep. // Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep.
const Vec3d c1(10., 10., 10.), c2(26., 10., 10.); const Vec3d c1(10., 10., 10.), c2(26., 10., 10.);
std::vector<ExPolygons> layers; std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices; std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 100; ++i) { for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.))); const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles; Polygons circles;
@@ -134,7 +134,7 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
} }
for (int i = 0; i < 100; ++i) for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]}); slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {}); const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
for (const Vec3d &c : {c1, c2}) { for (const Vec3d &c : {c1, c2}) {
CAPTURE(c.x()); CAPTURE(c.x());
@@ -143,7 +143,7 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance)); CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance));
} }
// The side between the lobes is half way to the surface, where both patterns morph into each other. // The side between the lobes is half way to the surface, where both patterns morph into each other.
TpmsRadialField::Radials radials; AdaptiveInfillField::Radials radials;
REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2); REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2);
for (size_t i = 0; i < 2; ++i) { for (size_t i = 0; i < 2; ++i) {
CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance)); CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance));
@@ -151,12 +151,12 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
} }
} }
TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field blends the lobes meeting at a junction continuously", "[AdaptiveInfillField]")
{ {
// Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle. // Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle.
const std::array<Vec3d, 3> centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)}; const std::array<Vec3d, 3> centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)};
std::vector<ExPolygons> layers; std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices; std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 100; ++i) { for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.))); const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles; Polygons circles;
@@ -169,7 +169,7 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
} }
for (int i = 0; i < 100; ++i) for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]}); slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {}); const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
// Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly. // Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly.
const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.; const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.;
@@ -179,7 +179,7 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
for (int row = 0; row <= 100; ++row) { for (int row = 0; row <= 100; ++row) {
std::array<float, 3> previous{}; std::array<float, 3> previous{};
for (int step = 0; step <= 200; ++step) { for (int step = 0; step <= 200; ++step) {
TpmsRadialField::Radials radials; AdaptiveInfillField::Radials radials;
const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials); const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials);
max_count = std::max(max_count, count); max_count = std::max(max_count, count);
std::array<float, 3> weights{}; std::array<float, 3> weights{};
@@ -201,23 +201,23 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
CHECK(max_jump < 0.05); CHECK(max_jump < 0.05);
} }
TEST_CASE("TPMS radial field is empty when the object is thinner than the grid cells", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field is empty when the object is thinner than the grid cells", "[AdaptiveInfillField]")
{ {
// A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more. // A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more.
const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)}; const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)};
std::vector<TpmsRadialField::Slice> slices; std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 1000; ++i) for (int i = 0; i < 1000; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &bar}); slices.push_back({0.2 * i, 0.2 * (i + 1), &bar});
const TpmsRadialField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)), const AdaptiveInfillField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
TpmsAdaptiveMode::Lobes, [] {}); AdaptiveInfillMode::Lobes, [] {});
CHECK(field.empty()); CHECK(field.empty());
} }
TEST_CASE("TPMS depth follows the distance to the surface relative to the deepest point", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill depth follows the distance to the surface relative to the deepest point", "[AdaptiveInfillField]")
{ {
// 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center. // 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center.
const ExPolygons square{rectangle(0., 0., 20., 20.)}; const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60., TpmsAdaptiveMode::SmoothBlend); const AdaptiveInfillField field = radial_field(square, 60., AdaptiveInfillMode::SmoothBlend);
for (double z : {15., 30., 45.}) { for (double z : {15., 30., 45.}) {
CAPTURE(z); CAPTURE(z);
CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance)); CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance));
@@ -227,18 +227,18 @@ TEST_CASE("TPMS depth follows the distance to the surface relative to the deepes
CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance)); CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance));
} }
TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the surface", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field in Distance warp mode follows the distance to the surface", "[AdaptiveInfillField]")
{ {
// In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes. // In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes.
const ExPolygons square{rectangle(0., 0., 20., 20.)}; const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField cube = radial_field(square, 20., TpmsAdaptiveMode::DistanceWarp); const AdaptiveInfillField cube = radial_field(square, 20., AdaptiveInfillMode::DistanceWarp);
CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance)); CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance)); CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance)); CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance));
// 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep. // 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep.
const TpmsRadialField lobes = radial_field(square, 60.); const AdaptiveInfillField lobes = radial_field(square, 60.);
const TpmsRadialField warp = radial_field(square, 60., TpmsAdaptiveMode::DistanceWarp); const AdaptiveInfillField warp = radial_field(square, 60., AdaptiveInfillMode::DistanceWarp);
for (double z : {15., 45.}) { for (double z : {15., 45.}) {
CAPTURE(z); CAPTURE(z);
CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance)); CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance));
@@ -247,12 +247,12 @@ TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the s
CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance)); CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance));
} }
TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill stepped shells split a layer by depth from the surface inwards", "[AdaptiveInfillField]")
{ {
// The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside. // The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside.
const ExPolygons square{rectangle(0., 0., 40., 40.)}; const ExPolygons square{rectangle(0., 0., 40., 40.)};
const TpmsRadialField field = radial_field(square, 40., TpmsAdaptiveMode::SteppedShells); const AdaptiveInfillField field = radial_field(square, 40., AdaptiveInfillMode::SteppedShells);
const std::vector<TpmsShell> shells = make_tpms_shells(field, square.front(), 20., 0.2f, 0.05f, TpmsAdaptiveGradient::Linear); const std::vector<AdaptiveInfillShell> shells = make_adaptive_infill_shells(field, square.front(), 20., 0.2f, 0.05f, AdaptiveInfillGradient::Linear);
REQUIRE(shells.size() == 5); REQUIRE(shells.size() == 5);
CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6)); CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6));
CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6)); CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6));
@@ -270,11 +270,11 @@ TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards",
[&center](const ExPolygon &expolygon) { return expolygon.contains(center); })); [&center](const ExPolygon &expolygon) { return expolygon.contains(center); }));
} }
TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on its own", "[FillTpmsAdaptive]") TEST_CASE("Adaptive infill field in 2D grades every section normal to the axis on its own", "[AdaptiveInfillField]")
{ {
// 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height. // 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height.
const ExPolygons square{rectangle(0., 0., 20., 20.)}; const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField normal_z = radial_field(square, 60., TpmsAdaptiveMode::NormalZ); const AdaptiveInfillField normal_z = radial_field(square, 60., AdaptiveInfillMode::NormalZ);
for (double z : {5., 30., 55.}) { for (double z : {5., 30., 55.}) {
CAPTURE(z); CAPTURE(z);
CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance)); CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance));
@@ -283,7 +283,7 @@ TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on it
} }
// Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom. // Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const TpmsRadialField normal_x = radial_field(square, 60., TpmsAdaptiveMode::NormalX); const AdaptiveInfillField normal_x = radial_field(square, 60., AdaptiveInfillMode::NormalX);
for (double x : {3., 10., 17.}) { for (double x : {3., 10., 17.}) {
CAPTURE(x); CAPTURE(x);
CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance)); CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance));
+13
View File
@@ -1457,6 +1457,19 @@ TEST_CASE("min_object_distance yields no floor when an FFF config lacks the opti
} }
} }
TEST_CASE("handle_legacy migrates the adaptive TPMS keys", "[Config]") {
ConfigSubstitutionContext ctxt(ForwardCompatibilitySubstitutionRule::Enable);
DynamicPrintConfig config;
config.set_deserialize("tpms_adaptive", "stepped_shells", ctxt);
config.set_deserialize("tpms_interior_density", "12%", ctxt);
config.set_deserialize("tpms_adaptive_gradient", "quadratic", ctxt);
CHECK(config.opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") == AdaptiveInfillMode::SteppedShells);
CHECK(config.option<ConfigOptionPercent>("adaptive_infill_interior_density")->value == Catch::Approx(12.));
CHECK(config.opt_enum<AdaptiveInfillGradient>("adaptive_infill_gradient") == AdaptiveInfillGradient::Quadratic);
CHECK(ctxt.substitutions.empty());
}
TEST_CASE("handle_legacy migrates every IDEX/IQEX key testers already have", "[Config]") { TEST_CASE("handle_legacy migrates every IDEX/IQEX key testers already have", "[Config]") {
// Three eras of saved profiles: the feature shipped as iXex (is_ixex + ixex_*), the clearance // Three eras of saved profiles: the feature shipped as iXex (is_ixex + ixex_*), the clearance
// pair was renamed to say what it measures, then the whole prefix became imex_. Driven from the // pair was renamed to say what it measures, then the whole prefix became imex_. Driven from the
+33
View File
@@ -0,0 +1,33 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "catch2/matchers/catch_matchers.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/EdgeGrid.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
TEST_CASE("EdgeGrid propagates signed distances across the interior and exterior", "[EdgeGrid][Regression]")
{
const Polygon rectangle {Point::new_scale(-10., -12.), Point::new_scale(10., -12.),
Point::new_scale(10., 12.), Point::new_scale(-10., 12.)};
const Polygons outlines {rectangle};
const BoundingBox bounds(Point::new_scale(-30., -32.), Point::new_scale(30., 32.));
EdgeGrid::Grid grid(bounds);
grid.create(outlines, scale_(1.));
// Distances beyond the initialized edge band require backward propagation.
// With -fsanitize=shift, this also checks negative neighbour offsets.
grid.calculate_sdf();
const auto distance = [&](double x, double y) {
return grid.signed_distance_bilinear(Point::new_scale(x, y)) * SCALING_FACTOR;
};
REQUIRE_THAT(distance(0., 0.), Catch::Matchers::WithinAbs(-10., 0.01));
for (double sign : {-1., 1.}) {
REQUIRE_THAT(distance(sign * 5., 0.), Catch::Matchers::WithinAbs(-5., 0.01));
REQUIRE_THAT(distance(0., sign * 7.), Catch::Matchers::WithinAbs(-5., 0.01));
REQUIRE_THAT(distance(sign * 20., 0.), Catch::Matchers::WithinAbs(10., 0.01));
REQUIRE_THAT(distance(0., sign * 22.), Catch::Matchers::WithinAbs(10., 0.01));
}
}
@@ -1,4 +1,5 @@
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <vector> #include <vector>
#include <cstddef> #include <cstddef>
@@ -12,10 +13,13 @@
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/GCode/OrderingStrategies.hpp" #include "libslic3r/GCode/OrderingStrategies.hpp"
#include "libslic3r/ShortestPath.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include <algorithm> #include <algorithm>
#include <unordered_set> #include <unordered_set>
#include <numeric>
#include <random>
#include <catch2/catch_approx.hpp> #include <catch2/catch_approx.hpp>
using namespace Slic3r; using namespace Slic3r;
@@ -304,3 +308,60 @@ TEST_CASE("snake_core handles three points", "[Snake]") {
REQUIRE(is_permutation(p2, 3)); REQUIRE(is_permutation(p2, 3));
} }
// The shortest cycle through the points, trying every order.
static double shortest_cycle_length(const Points& centers)
{
std::vector<size_t> path(centers.size());
std::iota(path.begin(), path.end(), 0);
double best = std::numeric_limits<double>::max();
do {
best = std::min(best, tsp_cycle_path_length(path, centers));
} while (std::next_permutation(path.begin() + 1, path.end()));
return best;
}
static Points random_centers(unsigned seed, size_t count)
{
std::mt19937 rng(seed);
std::uniform_real_distribution<> coordinate(0., 200.);
Points centers;
for (size_t i = 0; i < count; ++i) {
// One draw per statement, as the evaluation order of function arguments is unspecified.
const double x = coordinate(rng);
const double y = coordinate(rng);
centers.emplace_back(Point::new_scale(x, y));
}
return centers;
}
TEST_CASE("tsp_exact_cycle finds the shortest cycle", "[TSPPostProcessing]") {
const size_t count = GENERATE(4, 5, 6, 8);
for (unsigned seed = 1; seed <= 10; ++seed) {
const Points centers = random_centers(seed, count);
const std::vector<size_t> path = tsp_exact_cycle(centers);
REQUIRE(is_permutation(path, count));
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
}
}
TEST_CASE("Default ordering tours up to ten points along the shortest cycle", "[TSPPostProcessing]") {
const bool from_start = GENERATE(false, true);
const Point start = Point::new_scale(-20., -20.);
for (unsigned seed = 1; seed <= 5; ++seed) {
const Points centers = random_centers(seed, 9);
const std::vector<size_t> path = chain_points_with_postprocessing(centers, from_start ? &start : nullptr);
REQUIRE(is_permutation(path, centers.size()));
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
}
}
TEST_CASE("Default ordering of many points is never longer than the greedy cycle", "[TSPPostProcessing]") {
for (unsigned seed = 1; seed <= 10; ++seed) {
const Points centers = random_centers(seed, 150);
const std::vector<size_t> path = chain_points_with_postprocessing(centers, nullptr);
REQUIRE(is_permutation(path, centers.size()));
CHECK(euclidean_path_length(path, centers) <= euclidean_path_length(chain_points(centers, nullptr), centers) + SCALED_EPSILON);
CHECK_FALSE(has_crossings(path, centers));
}
}
+202
View File
@@ -0,0 +1,202 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/ExtrusionEntityCollection.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/ShortestPath.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include <map>
#include <numeric>
#include <math.h>
#include <random>
#include <utility>
#include <vector>
using namespace Slic3r;
namespace {
// Infill-like paths: lines of 1 to 30 mm scattered over a 100 mm square. The collection owns them, and entities hands
// them out as the raw pointers chain_and_reorder_extrusion_entities() reorders.
struct Paths
{
ExtrusionEntityCollection collection;
std::vector<ExtrusionEntity*> entities;
Paths(unsigned seed, size_t count, bool can_reverse)
{
std::mt19937 rng(seed);
std::uniform_real_distribution<> coordinate(0., 100.), length(1., 30.), angle(0., 2. * M_PI);
Polylines lines;
for (size_t i = 0; i < count; ++ i) {
// One draw per statement, as the evaluation order of function arguments is unspecified.
const double x = coordinate(rng);
const double y = coordinate(rng);
const double l = length(rng);
const double t = angle(rng);
const Vec2d a(x, y);
const Vec2d b = a + l * Vec2d(std::cos(t), std::sin(t));
lines.emplace_back(Point::new_scale(a), Point::new_scale(b));
}
extrusion_entities_append_paths(collection.entities, std::move(lines), erInternalInfill, 0.1, 0.45f, 0.2f, can_reverse);
entities = collection.entities;
}
};
double travel(const std::vector<ExtrusionEntity*> &entities, const Point &start)
{
double length = 0.;
Point last = start;
for (const ExtrusionEntity *ee : entities) {
length += (ee->first_point() - last).cast<double>().norm();
last = ee->last_point();
}
return length;
}
// Travel of the chain that always goes on to the nearest free path end.
double nearest_next_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
{
std::vector<char> taken(entities.size(), 0);
double length = 0.;
Point last = from;
for (size_t step = 0; step < entities.size(); ++ step) {
double best = std::numeric_limits<double>::max();
size_t best_idx = 0;
bool best_reversed = false;
for (size_t i = 0; i < entities.size(); ++ i)
for (const bool reversed : { false, true })
if (! taken[i] && (can_reverse || ! reversed)) {
const double d = ((reversed ? entities[i]->last_point() : entities[i]->first_point()) - last).cast<double>().norm();
if (d < best) {
best = d;
best_idx = i;
best_reversed = reversed;
}
}
taken[best_idx] = 1;
length += best;
last = best_reversed ? entities[best_idx]->first_point() : entities[best_idx]->last_point();
}
return length;
}
// The shortest travel over every order and allowed reversal of the paths, from start.
double shortest_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
{
std::vector<size_t> order(entities.size());
std::iota(order.begin(), order.end(), 0);
double best = std::numeric_limits<double>::max();
do {
for (size_t mask = 0; mask < (size_t(1) << order.size()) && (can_reverse || mask == 0); ++ mask) {
double length = 0.;
Point last = from;
for (size_t k = 0; k < order.size(); ++ k) {
const ExtrusionEntity *ee = entities[order[k]];
const bool reversed = (mask >> k & 1) != 0;
length += ((reversed ? ee->last_point() : ee->first_point()) - last).cast<double>().norm();
last = reversed ? ee->first_point() : ee->last_point();
}
best = std::min(best, length);
}
} while (std::next_permutation(order.begin(), order.end()));
return best;
}
const Point start = Point::new_scale(-10., -10.);
} // namespace
TEST_CASE("An improved infill chain is never longer than the greedy or nearest-next one and keeps every path", "[ShortestPath]")
{
const size_t count = GENERATE(2, 3, 10, 60, 300);
const bool can_reverse = GENERATE(true, false);
double greedy_total = 0., improved_total = 0.;
for (unsigned seed = 1; seed <= 20; ++ seed) {
Paths greedy(seed, count, can_reverse), improved(seed, count, can_reverse);
const double nearest_travel = nearest_next_travel(improved.entities, can_reverse, start);
chain_and_reorder_extrusion_entities(greedy.entities, &start);
chain_and_reorder_extrusion_entities(improved.entities, start);
std::vector<ExtrusionEntity*> sorted = improved.entities;
std::sort(sorted.begin(), sorted.end());
REQUIRE(std::unique(sorted.begin(), sorted.end()) == sorted.end());
REQUIRE(sorted.size() == count);
const double greedy_travel = travel(greedy.entities, start), improved_travel = travel(improved.entities, start);
CHECK(improved_travel <= greedy_travel + SCALED_EPSILON);
CHECK(improved_travel <= nearest_travel + SCALED_EPSILON);
greedy_total += greedy_travel;
improved_total += improved_travel;
}
// Twenty random sets of ten or more paths always leave the greedy chain something to shorten.
if (count >= 10)
CHECK(improved_total < 0.95 * greedy_total);
}
TEST_CASE("Improving an infill chain keeps the direction of paths that cannot reverse", "[ShortestPath]")
{
Paths paths(7, 100, false);
std::map<const ExtrusionEntity*, Point> first_points;
for (const ExtrusionEntity *ee : paths.entities)
first_points.emplace(ee, ee->first_point());
chain_and_reorder_extrusion_entities(paths.entities, start);
for (const ExtrusionEntity *ee : paths.entities)
CHECK(ee->first_point() == first_points.at(ee));
}
TEST_CASE("Chains of a few paths are the shortest possible", "[ShortestPath]")
{
const size_t count = GENERATE(2, 3, 4, 5, 6);
const bool can_reverse = GENERATE(true, false);
for (unsigned seed = 1; seed <= 10; ++ seed) {
Paths paths(seed, count, can_reverse);
const double shortest = shortest_travel(paths.entities, can_reverse, start);
chain_and_reorder_extrusion_entities(paths.entities, start);
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
}
}
TEST_CASE("The greedy chain of two to four paths is the shortest possible", "[ShortestPath]")
{
const size_t count = GENERATE(2, 3, 4);
const bool can_reverse = GENERATE(true, false);
for (unsigned seed = 1; seed <= 10; ++ seed) {
Paths paths(seed, count, can_reverse);
const double shortest = shortest_travel(paths.entities, can_reverse, start);
chain_and_reorder_extrusion_entities(paths.entities, &start);
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
}
}
TEST_CASE("A short chain need not start with the path nearest to its start", "[ShortestPath]")
{
// The nearest end leads 10 mm away from the two other paths, so the shortest chain leaves that path for last.
const Point from = Point::new_scale(0., 0.);
for (const bool greedy : { false, true }) {
ExtrusionEntityCollection collection;
extrusion_entities_append_paths(collection.entities,
Polylines{ Polyline(Point::new_scale(-0.9, 0.), Point::new_scale(-10., 0.)), Polyline(Point::new_scale(1., 0.), Point::new_scale(2., 0.)),
Polyline(Point::new_scale(3., 1.), Point::new_scale(4., 1.)) },
erInternalInfill, 0.1, 0.45f, 0.2f);
std::vector<ExtrusionEntity*> entities = collection.entities;
const double shortest = shortest_travel(entities, true, from);
if (greedy)
chain_and_reorder_extrusion_entities(entities, &from);
else
chain_and_reorder_extrusion_entities(entities, from);
CHECK(entities.back() == collection.entities.front());
CHECK_THAT(travel(entities, from), Catch::Matchers::WithinAbs(shortest, SCALED_EPSILON));
}
}
+76
View File
@@ -0,0 +1,76 @@
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/TexturePainting.hpp"
#include "texture_import_cgal.hpp"
#include <algorithm>
#include <array>
#include <string>
#include <vector>
namespace {
void check_texture_import(const std::string& fixture)
{
const std::string filename = std::string(TEST_DATA_DIR) + "/texture_import/" + fixture;
INFO(filename);
const Slic3r::Model model = Slic3r::Model::read_from_file(filename);
REQUIRE_FALSE(model.objects.empty());
REQUIRE(model.texture_mesh);
const Slic3r::TexturedMesh& mesh = *model.texture_mesh;
Slic3r::TexturePaintingSettings settings;
settings.target_colors_num = 0; // Match the dialog's initial automatic color count.
settings.smooth_weight = 0.5;
settings.mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
Slic3r::PaintedMesh painted;
// Follow the dialog worker's format-dependent path, including the boundary
// smoothing that needs a consistent CGAL Kd_tree layout across libraries.
if (!mesh.precomputed_face_colors.empty())
REQUIRE(Slic3r::face_colors_to_painting(mesh, painted, settings));
else
REQUIRE(Slic3r::texture_to_painting(mesh, painted, settings));
REQUIRE_FALSE(painted.indices.empty());
REQUIRE(painted.face_colors.size() == painted.indices.size());
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
[](const auto& color) { return color[0] > color[2]; }));
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
[](const auto& color) { return color[2] > color[0]; }));
const std::vector<std::array<float, 4>> filament_colors{
{1.f, 0.f, 0.f, 1.f}, {0.f, 0.f, 1.f, 1.f}};
const auto matches = Slic3r::match_clusters_to_filaments(
painted.cluster_colors, filament_colors, {"Red PLA", "Blue PLA"});
REQUIRE(matches.size() == painted.cluster_colors.size());
REQUIRE(std::any_of(matches.begin(), matches.end(),
[](const auto& match) { return match.filament_index == 0; }));
REQUIRE(std::any_of(matches.begin(), matches.end(),
[](const auto& match) { return match.filament_index == 1; }));
}
} // namespace
TEST_CASE("Color-boundary smoothing shares its CGAL tree layout with the geometry backend", "[TextureImport][CGAL][Regression]")
{
// An ABI mismatch need not crash every link of the importer. Compare the
// layouts directly so a lucky choice of inline symbols cannot mask it.
REQUIRE(sizeof(TextureImportTest::SearchTree) == TextureImportTest::backend_search_tree_size());
}
TEST_CASE("A JPEG textured GLB retains both colors through filament matching", "[TextureImport][Regression]")
{
check_texture_import("two_color_jpeg.glb");
}
TEST_CASE("An OBJ preserves both MTL colors through filament matching", "[TextureImport][OBJ][Regression]")
{
check_texture_import("two_color_sphere/test_two_color_sphere.obj");
}
TEST_CASE("An OBJ resolves its MTL and external JPEG through filament matching", "[TextureImport][OBJ][Regression]")
{
check_texture_import("two_color_sphere/test_two_color_sphere_textured.obj");
}
+8
View File
@@ -0,0 +1,8 @@
#include "texture_import_cgal.hpp"
#include <cstddef>
std::size_t TextureImportTest::backend_search_tree_size()
{
return sizeof(SearchTree);
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <CGAL/AABB_face_graph_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Kd_tree.h>
#include <CGAL/Search_traits_adapter.h>
#include <CGAL/Surface_mesh/Surface_mesh.h>
#include <CGAL/property_map.h>
#include <cstddef>
namespace TextureImportTest {
// The distance-query tree used by ColorUtils.cpp's boundary smoothing. Its
// type name stays the same when CGAL changes its storage for TBB support.
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
using Mesh = CGAL::Surface_mesh<Kernel::Point_3>;
using Primitive = CGAL::AABB_face_graph_triangle_primitive<Mesh>;
using Traits = CGAL::AABB_traits_3<Kernel, Primitive>;
using PointId = Traits::Point_and_primitive_id;
using SearchTraits = CGAL::Search_traits_adapter<PointId, CGAL::First_of_pair_property_map<PointId>, Traits>;
using SearchTree = CGAL::Kd_tree<SearchTraits>;
// Implemented inside the actual geometry target, using its private definitions.
std::size_t backend_search_tree_size();
} // namespace TextureImportTest
+1
View File
@@ -44,6 +44,7 @@ add_executable(${_TEST_NAME}_tests
test_shortcuts.cpp test_shortcuts.cpp
test_texture_color_palette.cpp test_texture_color_palette.cpp
test_texture_island_view.cpp test_texture_island_view.cpp
test_timelapse_codecs.cpp
test_file_url.cpp test_file_url.cpp
test_imgui_wrapper.cpp test_imgui_wrapper.cpp
test_user_manager.cpp test_user_manager.cpp
@@ -81,3 +81,24 @@ TEST_CASE("Payload rewrite covers nested trays, calibration lists and mapping in
CHECK(BBLPrinterAgent::from_orca_payload("not json") == "not json"); CHECK(BBLPrinterAgent::from_orca_payload("not json") == "not json");
CHECK(BBLPrinterAgent::from_orca_payload(R"({"print":{"command":"pushall"}})") == R"({"print":{"command":"pushall"}})"); CHECK(BBLPrinterAgent::from_orca_payload(R"({"print":{"command":"pushall"}})") == R"({"print":{"command":"pushall"}})");
} }
TEST_CASE("Payload rewrite covers the nozzle rack status and the auto nozzle mapping request", "[BambuFilamentIds]")
{
const ScopedRepoResourcesDir repo_resources;
const std::string petg_hf = orca_id_of("GFG96");
const std::string rack_status =
R"({"print":{"device":{"nozzle":{"info":[{"id":16,"fila_id":"GFG96","color_m":"FFFFFFFF"},{"id":17,"fila_id":"P22f54c9"}]}}}})";
json inbound = json::parse(BBLPrinterAgent::to_orca_payload(rack_status));
CHECK(inbound["print"]["device"]["nozzle"]["info"][0]["fila_id"] == petg_hf);
CHECK(inbound["print"]["device"]["nozzle"]["info"][1]["fila_id"] == "P22f54c9"); // user id: untouched
const std::string request = R"({"print":{"command":"get_auto_nozzle_mapping","ams_mapping":[65535,65024],)"
R"("fila_info":[{"id":2,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"}],)"
R"("nozzle_info":[{"pos":16,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"},{"pos":1,"cate":""}]}})";
json outbound = json::parse(BBLPrinterAgent::from_orca_payload(request));
CHECK(outbound["print"]["fila_info"][0]["cate"] == "GFG96");
CHECK(outbound["print"]["nozzle_info"][0]["cate"] == "GFG96");
CHECK(outbound["print"]["nozzle_info"][1]["cate"] == ""); // empty nozzle
CHECK(outbound["print"]["ams_mapping"] == json::parse("[65535,65024]"));
}
@@ -420,12 +420,13 @@ TEST_CASE("Moonraker discovery waits for a complete device identity", "[Moonrake
agent.device_info.dev_id = "127.0.0.1:7125"; agent.device_info.dev_id = "127.0.0.1:7125";
agent.device_info.dev_ip = agent.device_info.dev_id; agent.device_info.dev_ip = agent.device_info.dev_id;
agent.device_info.base_url = "http://127.0.0.1:7125"; agent.device_info.base_url = "http://127.0.0.1:7125";
agent.device_info.model_name = "Test model";
agent.start_discovery(true, false); agent.start_discovery(true, false);
REQUIRE(announcements == 1); REQUIRE(announcements == 1);
REQUIRE(agent.fetch_count == 1); REQUIRE(agent.fetch_count == 0);
REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id); REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id);
REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip); REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip);
REQUIRE(announcement.at("dev_name") == "Test printer"); REQUIRE(announcement.at("dev_name") == "Test model");
} }
TEST_CASE("Stopping the status stream gives a silent printer no more than its timeouts", "[MoonrakerPrinterAgent][StatusStream]") TEST_CASE("Stopping the status stream gives a silent printer no more than its timeouts", "[MoonrakerPrinterAgent][StatusStream]")
+59 -8
View File
@@ -8,6 +8,7 @@
#include <boost/asio/ip/address.hpp> #include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp> #include <boost/asio/ip/tcp.hpp>
#include <boost/asio/read_until.hpp> #include <boost/asio/read_until.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/streambuf.hpp> #include <boost/asio/streambuf.hpp>
#include <boost/asio/write.hpp> #include <boost/asio/write.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
@@ -91,7 +92,9 @@ using boost::asio::ip::tcp;
class LoopbackServer class LoopbackServer
{ {
public: public:
enum class Mode { Ok, Unavailable, Silent }; // Session answers every request with the body given to set_session_body(), as the token endpoint
// does, after 200 ms so the client is still waiting when the test makes its next call.
enum class Mode { Ok, Unavailable, Silent, Session };
struct Request struct Request
{ {
@@ -113,6 +116,11 @@ public:
std::string url() const { return "http://127.0.0.1:" + std::to_string(m_acceptor.local_endpoint().port()); } std::string url() const { return "http://127.0.0.1:" + std::to_string(m_acceptor.local_endpoint().port()); }
void set_mode(Mode mode) { m_mode = mode; } void set_mode(Mode mode) { m_mode = mode; }
void set_session_body(std::string body)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_session_body = std::move(body);
}
bool wait_for_accepts(size_t count, std::chrono::milliseconds timeout) bool wait_for_accepts(size_t count, std::chrono::milliseconds timeout)
{ {
@@ -172,13 +180,31 @@ private:
const Mode mode = m_mode; const Mode mode = m_mode;
if (mode == Mode::Silent) if (mode == Mode::Silent)
return; return;
const std::string_view response = mode == Mode::Ok ? auto response = std::make_shared<std::string>();
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" : if (mode == Mode::Session) {
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; std::lock_guard<std::mutex> lock(m_mutex);
boost::asio::async_write(connection->socket, boost::asio::buffer(response), *response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " +
[connection](boost::system::error_code, size_t) { std::to_string(m_session_body.size()) + "\r\nConnection: close\r\n\r\n" + m_session_body;
boost::system::error_code ignored; } else {
connection->socket.shutdown(tcp::socket::shutdown_both, ignored); *response = mode == Mode::Ok ?
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" :
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
}
auto write = [connection, response] {
boost::asio::async_write(connection->socket, boost::asio::buffer(*response),
[connection, response](boost::system::error_code, size_t) {
boost::system::error_code ignored;
connection->socket.shutdown(tcp::socket::shutdown_both, ignored);
});
};
if (mode != Mode::Session) {
write();
return;
}
auto timer = std::make_shared<boost::asio::steady_timer>(m_io, 200ms);
timer->async_wait([timer, write](boost::system::error_code ec) {
if (!ec)
write();
}); });
}); });
} }
@@ -192,8 +218,12 @@ private:
std::condition_variable m_cv; std::condition_variable m_cv;
std::vector<Request> m_requests; std::vector<Request> m_requests;
std::vector<std::shared_ptr<Connection>> m_connections; std::vector<std::shared_ptr<Connection>> m_connections;
std::string m_session_body;
}; };
// A JWT whose only claim is {"exp":4102444800} (2100-01-01); the agent reads nothing else from it.
constexpr const char* FAR_FUTURE_JWT = "eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.sig";
// What the agent reported through its callbacks, recorded on whichever thread delivers it so the // What the agent reported through its callbacks, recorded on whichever thread delivers it so the
// assertions can run on the test thread. // assertions can run on the test thread.
struct CallbackLog struct CallbackLog
@@ -515,3 +545,24 @@ TEST_CASE_METHOD(HealthCheckFixture, "The health check does no token work", "[Or
CHECK(agent->is_user_login()); CHECK(agent->is_user_login());
CHECK(agent->get_access_token() == "test-token"); CHECK(agent->get_access_token() == "test-token");
} }
TEST_CASE_METHOD(HealthCheckFixture, "A token request made while start() refreshes waits for it instead of refreshing again", "[OrcaCloudServiceAgent]")
{
// A session saved by a previous run: start() refreshes it on its own thread.
agent->persist_user_secret(nlohmann::json{{"refresh_token", "stored-refresh-token"}, {"user_id", "test-user-id"}}.dump());
const std::string access_token = FAR_FUTURE_JWT;
server.set_session_body(nlohmann::json{{"access_token", access_token},
{"refresh_token", "rotated-refresh-token"},
{"user_id", "test-user-id"}}.dump());
server.set_mode(LoopbackServer::Mode::Session);
REQUIRE(agent->start() == BAMBU_NETWORK_SUCCESS);
CHECK(agent->ensure_token_fresh("test"));
CHECK(agent->get_access_token() == access_token);
size_t token_posts = 0;
for (const auto& request : server.requests())
if (boost::starts_with(request.request_line, "POST /auth/v1/token"))
++token_posts;
CHECK(token_posts == 1);
}
+100
View File
@@ -0,0 +1,100 @@
#include <catch2/catch_test_macros.hpp>
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/TexturePainting.hpp"
#include "slic3r/GUI/AVVideoDecoder.hpp"
#include "slic3r/GUI/Printer/BambuTunnel.h"
#include <atomic>
#include <cstddef>
#include <cstdlib>
#include <thread>
#include <vector>
#include <wx/bitmap.h>
#include <wx/gdicmn.h>
#include <wx/image.h>
#include <wx/init.h>
#include <wx/mstream.h>
TEST_CASE("JPEG texture decoding coexists with thumbnails and video frames", "[TimelapseCodecs][Regression]")
{
#if defined(__linux__)
if (!std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY"))
SKIP("This integration test needs a display (for example, xvfb-run).");
#endif
wxInitializer init;
REQUIRE(init.IsOk());
wxInitAllImageHandlers();
// Generate a JPEG through the same encoder used for G-code thumbnails.
constexpr int width = 160, height = 90, iterations = 1000;
Slic3r::ThumbnailData thumbnail;
thumbnail.set(width, height);
for (std::size_t i = 0; i < thumbnail.pixels.size(); i += 4) {
thumbnail.pixels[i] = 180;
thumbnail.pixels[i + 1] = 70;
thumbnail.pixels[i + 2] = 30;
thumbnail.pixels[i + 3] = 255;
}
const auto jpeg = Slic3r::GCodeThumbnails::compress_thumbnail(thumbnail, Slic3r::GCodeThumbnailsFormat::JPG);
REQUIRE(jpeg);
REQUIRE(jpeg->size > 0);
const auto* bytes = static_cast<const unsigned char*>(jpeg->data);
Slic3r::TextureImage texture;
texture.width = texture.height = -1;
texture.data.assign(bytes, bytes + jpeg->size);
std::atomic<bool> texture_ok{true};
std::thread texture_worker([&] {
for (int i = 0; i < iterations; ++i) {
std::vector<unsigned char> pixels;
int decoded_width = 0, decoded_height = 0;
if (!Slic3r::decode_texture_to_pixels(texture, pixels, decoded_width, decoded_height) ||
decoded_width != width || decoded_height != height || pixels.size() != width * height * 3)
texture_ok = false;
}
});
// PrinterFileSystem decodes timelapse thumbnails with wxImage. Do not
// assert until the worker is joined: Catch assertions can unwind the scope.
bool thumbnails_ok = true;
for (int i = 0; i < iterations; ++i) {
wxMemoryInputStream stream(jpeg->data, jpeg->size);
wxImage image(stream, "image/jpeg");
if (!image.IsOk() || image.GetWidth() != width || image.GetHeight() != height ||
image.GetRed(width / 2, height / 2) < 170 || image.GetGreen(width / 2, height / 2) > 80)
thumbnails_ok = false;
}
texture_worker.join();
REQUIRE(texture_ok.load());
REQUIRE(thumbnails_ok);
// Exercise Orca's real FFmpeg decoder and its platform display conversion.
AVVideoDecoder decoder;
Bambu_StreamInfo info{};
info.type = VIDE;
info.sub_type = MJPG;
info.format_type = video_jpeg;
REQUIRE(decoder.open(info) == 0);
Bambu_Sample sample{};
sample.size = static_cast<int>(jpeg->size);
sample.buffer = bytes;
for (int i = 0; i < iterations; ++i) {
REQUIRE(decoder.decode(sample) == 0);
wxImage image;
REQUIRE(decoder.toWxImage(image, wxSize(width, height)));
REQUIRE(image.IsOk());
REQUIRE(image.GetWidth() == width);
REQUIRE(image.GetHeight() == height);
#ifdef _WIN32
// Playback uses this raw 32-bit bitmap conversion only on Windows.
wxBitmap bitmap;
REQUIRE(decoder.toWxBitmap(bitmap, wxSize(width, height)));
REQUIRE(bitmap.IsOk());
REQUIRE(bitmap.GetWidth() == width);
REQUIRE(bitmap.GetHeight() == height);
#endif
}
}