mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 06:01:16 +00:00
Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9d1bb85b8 | ||
|
|
c9d897b4d0 | ||
|
|
6bff3c3cd6 | ||
|
|
6e1d15f8a2 | ||
|
|
4f63cd4051 | ||
|
|
c7489d431d | ||
|
|
506dd497db | ||
|
|
56da2f43b0 | ||
|
|
00838d59c1 | ||
|
|
8f8bfd6a28 | ||
|
|
90a5a0b964 | ||
|
|
f64beb4379 | ||
|
|
a2b1a2f3e7 | ||
|
|
0585836550 | ||
|
|
5a36120c58 | ||
|
|
69193d5681 | ||
|
|
cdb9bb57ae | ||
|
|
f0c59208e7 | ||
|
|
9f6a344162 |
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
Vendored
+5
-2
@@ -26,6 +26,7 @@ orcaslicer_add_cmake_project(OpenCV
|
|||||||
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
|
URL 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
|
||||||
|
|||||||
Vendored
+9
-1
@@ -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.
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# Travel optimization — High Level Design
|
||||||
|
|
||||||
|
## Purpose and scope
|
||||||
|
|
||||||
|
Inside an island, the infill paths of each region are chained into one print
|
||||||
|
order by a greedy pass that joins the closest free path ends first. That pass
|
||||||
|
leaves its longest joins for last, and they often cross the whole island. Two
|
||||||
|
things shorten the travel between infill paths:
|
||||||
|
|
||||||
|
- the fill generators join their lines into as few paths as they can, since
|
||||||
|
every separate path needs a travel to it;
|
||||||
|
- the G-code export refines the greedy chain with a local search that shortens
|
||||||
|
the straight-line travel between the paths.
|
||||||
|
|
||||||
|
The refinement covers every chain that `chain_and_reorder_extrusion_entities()`
|
||||||
|
builds from a start point: the infill and ironing of `GCode::extrude_infill()`
|
||||||
|
and the support of `GCode::extrude_support()`, as well as the order of objects
|
||||||
|
and islands under `print_order` Default. It changes only the order and the
|
||||||
|
direction of the paths, not the paths or the walls. Neither part has a setting.
|
||||||
|
|
||||||
|
## The chain
|
||||||
|
|
||||||
|
`ObjectByExtruder::Island::Region::append()` flattens every sortable fill
|
||||||
|
collection into its paths, so the paths of all the fills of a region in one
|
||||||
|
island are chained together. A collection that must keep its order (`no_sort`:
|
||||||
|
monotonic, concentric, a forced fill order, the flow calibration) stays a single
|
||||||
|
entry that cannot be reversed. Paths that must keep their direction cannot be
|
||||||
|
reversed either: Grid's, since it alternates their direction between layers for
|
||||||
|
strength, and those of a fill with a forced order. A loop starts and ends at the
|
||||||
|
same point, so reversing it changes nothing.
|
||||||
|
|
||||||
|
`extrude_infill()` chains each region from the current nozzle position with
|
||||||
|
`chain_and_reorder_extrusion_entities()`, and chains the paths inside a nested
|
||||||
|
sortable collection the same way; `extrude_support()` chains the support paths
|
||||||
|
of a layer so too, unless they keep their order. The overload without a start
|
||||||
|
point, used to chain collections while slicing, keeps the greedy chain. The
|
||||||
|
order the fill generators give their paths does not reach the G-code for a
|
||||||
|
sortable fill, which is chained again.
|
||||||
|
|
||||||
|
## Connecting the lines
|
||||||
|
|
||||||
|
Most patterns cut their lines at the infill boundary, and `Fill::connect_infill()`
|
||||||
|
joins pairs of line ends that are neighbors on a boundary contour by the arc of
|
||||||
|
wall between them, shortest arcs first, as long as neither end is joined yet and
|
||||||
|
the join closes no loop. An arc is joined when it is shorter than
|
||||||
|
`infill_anchor_max`, or than two anchors and a line width if that is longer:
|
||||||
|
two ends further apart each get an anchor toward the other, and where those
|
||||||
|
would meet, the join prints about as much wall as one path instead of two.
|
||||||
|
|
||||||
|
Regular patterns leave many arcs of the same length, and the order among them
|
||||||
|
decides the pairing. In an arbitrary order the pairs along one wall can fall out
|
||||||
|
of step, such as lines 1–2 and 3–4, then 6–7 and 8–9, which leaves the end of
|
||||||
|
line 5 loose, and every loose end starts or ends a separate path. So the arcs
|
||||||
|
are also sorted in steps of half the line spacing and, within a step, along each
|
||||||
|
contour forwards and backwards, which pairs the ends of a wall in one phase.
|
||||||
|
Each order is replayed on the flags and a union-find of the lines, without
|
||||||
|
building any path, and the one that joins the most lines is used; a tie keeps
|
||||||
|
the shortest-first order.
|
||||||
|
|
||||||
|
The replay includes the last pass, which joins any two open ends that are
|
||||||
|
neighbors on a contour. With multiline infill the main pass leaves the copies of
|
||||||
|
one line to that pass, so that each copy reaches a wall of its own. Two
|
||||||
|
neighboring groups cut at both walls can then be joined at either wall by arcs
|
||||||
|
of the same length: at one, their open ends stay next to their own copies, which
|
||||||
|
the last pass joins, so each line prints out and back; at the other, the copies
|
||||||
|
end up in separate paths printed apart.
|
||||||
|
|
||||||
|
A line end left unpaired gets an anchor, a piece of the wall at most
|
||||||
|
`infill_anchor` long. It runs toward the nearest open end of another path,
|
||||||
|
looking up to 64 ends along the contour, since the next path usually starts
|
||||||
|
there; without one it takes the side with more free wall. With multiline infill
|
||||||
|
an anchor toward a copy of the same line stops short of the copy rather than
|
||||||
|
printing over it. A copy is a neighboring end less than the group width
|
||||||
|
(`multiline` spacings) across the line, at any wall angle, while the next group
|
||||||
|
lies a whole line spacing away. Meeting the wall at an angle θ, the copy covers
|
||||||
|
half a width over sin θ of the wall on each side of its end, so the anchor stops
|
||||||
|
that far and half its own width before it, or is left out; the end still counts
|
||||||
|
as anchored, so the last pass pairs the other ends as before.
|
||||||
|
|
||||||
|
Last, `Fill::fill_surface_extrusion()` joins the sparse infill lines whose ends
|
||||||
|
are at most two line spacings apart with a straight piece. With the anchors off
|
||||||
|
(`infill_anchor_max` 0) nothing above runs and this is the only joining; with
|
||||||
|
them on, it takes what the steps above leave next to each other, such as two
|
||||||
|
anchors that stop just short of each other or a short piece cut off by a corner.
|
||||||
|
A gap that short leaves no room to print anything in between, and as a travel it
|
||||||
|
is the one hop a reordering can trade for a longer travel that retracts.
|
||||||
|
Multiline copies leave the most such ends: one spacing apart across the line,
|
||||||
|
their ends where it meets a wall at an angle θ are one spacing over sin θ apart
|
||||||
|
along it, so two spacings reach walls met at 30° and steeper.
|
||||||
|
|
||||||
|
Ends that meet are joined first, then the gaps between the leading ends of their
|
||||||
|
lines, along each line's overall direction, then the rest, shortest first within
|
||||||
|
each, without closing a loop or crossing a line or another join. Taking the
|
||||||
|
leading ends first joins every pair of copies at the same side, which leaves
|
||||||
|
their open ends next to each other; by length alone, neighboring pairs were
|
||||||
|
joined at different walls and the travel between them crossed the island. Lines
|
||||||
|
that cannot reverse, such as Grid's, only join the end of one to the start of the
|
||||||
|
next, and fills that keep their order are left alone.
|
||||||
|
|
||||||
|
## The search
|
||||||
|
|
||||||
|
A chain of up to eight paths is found exactly, by dynamic programming over the
|
||||||
|
subsets of paths: a state is the paths visited so far, the last one and its
|
||||||
|
direction, about four thousand states for eight paths. The shared greedy
|
||||||
|
chaining, `chain_segments_greedy_constrained_reversals_()`, does the same for two
|
||||||
|
to four segments, which also orders up to four islands of a layer and the loops
|
||||||
|
of a perimeter collection exactly. Either way the first path is no longer
|
||||||
|
always the one nearest the start; with a free end, a path near the start can be
|
||||||
|
worth leaving for last.
|
||||||
|
|
||||||
|
Objects (`print_order` Default) and the tour of islands across objects are
|
||||||
|
visited in the same order on every layer, so `chain_points_with_postprocessing()`
|
||||||
|
orders them as a closed tour rather than an open chain. Up to ten points the
|
||||||
|
shortest tour is found exactly by the same dynamic programming with the first
|
||||||
|
point fixed; with a start point the tour then begins at the point nearest it.
|
||||||
|
Larger sets take the greedy chain and alternate 2-opt, or-opt and crossing
|
||||||
|
removal; or-opt moves runs of up to three points next to one of the eight points
|
||||||
|
nearest an end of the run, found on the same grid as the chain's candidates.
|
||||||
|
|
||||||
|
For longer chains, the search refines the greedy chain. On an island cut into many short pieces,
|
||||||
|
such as infill without anchors, that chain can sweep away from the nozzle and
|
||||||
|
come back for the rest with one long jump, which no local move below removes.
|
||||||
|
So a nearest-next chain, going from the nozzle to the nearest free path end each
|
||||||
|
time, is built too; when it is already shorter than the refined greedy chain, it
|
||||||
|
is refined and used instead. Elsewhere it costs one grid query per path.
|
||||||
|
|
||||||
|
`improve_chain_by_local_search()` sees the chain as a sequence of segments with
|
||||||
|
an entry and an exit point each. Its cost is the straight distance from the
|
||||||
|
nozzle to the first entry plus the distance from every exit to the next entry;
|
||||||
|
the end of the chain is free. It applies three kinds of moves:
|
||||||
|
|
||||||
|
- 2-opt reverses a run of segments, which flips each of them and replaces the
|
||||||
|
two links at the ends of the run.
|
||||||
|
- Or-opt moves a run of up to three segments between two others, flipped or
|
||||||
|
not.
|
||||||
|
- A run of any length that starts or ends next to the nozzle moves to the front.
|
||||||
|
Local search can settle on a chain that starts with a block of paths far from
|
||||||
|
the nozzle while the paths next to it sit in the middle, and moving that block
|
||||||
|
takes more than three segments at once.
|
||||||
|
|
||||||
|
A move that would flip a segment that cannot be reversed is skipped, so such
|
||||||
|
segments are only reordered.
|
||||||
|
|
||||||
|
A move is tried only if every link it creates joins an end point to one of the
|
||||||
|
32 end points nearest to it, which covers every move on a chain of up to 17
|
||||||
|
segments, as most islands are; with 8 candidates the search missed most of the
|
||||||
|
gain on grid-like patterns such as Triangles. The candidates come from a uniform
|
||||||
|
grid of about two end points per cell, searched in rings of cells around the
|
||||||
|
query. `KDTreeIndirect` visits its subtrees in a fixed order rather than the
|
||||||
|
nearer one first, so a query for 32 points walks most of the tree, and on long
|
||||||
|
chains finding the candidates would cost more than the search itself.
|
||||||
|
|
||||||
|
The candidates of an end point are tried nearest first, and only while they are
|
||||||
|
closer to it than the link the move would replace there. A 2-opt move that pays
|
||||||
|
off creates a link shorter than the one it replaces at one of its two ends, so
|
||||||
|
this misses no 2-opt move. An or-opt move is tried only while its new link is
|
||||||
|
shorter than what taking the run out saves, which skips a few or-opt moves that
|
||||||
|
would still pay off but cuts the search on long chains to a fraction.
|
||||||
|
|
||||||
|
The search works through a queue of segments and applies the first move that
|
||||||
|
shortens the chain around the segment it examines. After a move, only the
|
||||||
|
segments next to a changed link are queued again. A new link can also make room
|
||||||
|
for a run elsewhere that no segment next to it would try, so when the queue
|
||||||
|
empties after any move, every segment is queued once more, and the search ends
|
||||||
|
when such a sweep finds no move.
|
||||||
|
|
||||||
|
Every move shortens the chain by more than `SCALED_EPSILON`, so the result is
|
||||||
|
never longer than the greedy or the nearest-next chain and the search ends; a
|
||||||
|
cap of 50 moves per segment only bounds its time. The search has no randomness
|
||||||
|
and visits the segments in a fixed order, so the same input always gives the
|
||||||
|
same G-code.
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
Finding the candidates of an end point visits a few cells, and the nearest-next
|
||||||
|
chain makes one such query per path, which only grows where the paths around the
|
||||||
|
nozzle are used up and the chain jumps. Examining a segment tries at most a few
|
||||||
|
hundred candidate links, usually only the nearest few, plus a scan of the chain
|
||||||
|
for the move to the front when it is the first segment, and a move rewrites only
|
||||||
|
the range of the chain it changes, so the search stays close to linear in the
|
||||||
|
number of segments. Most islands hold a few to a few dozen paths; the cost shows
|
||||||
|
on infill cut into thousands of pieces per island, such as dense infill with the
|
||||||
|
anchors off. The straight joins only look at the line segments near a candidate
|
||||||
|
join for crossings.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
The search minimizes straight-line distance. It does not know which travels
|
||||||
|
leave the infill area and retract under `reduce_infill_retraction`, nor the
|
||||||
|
detours of `reduce_crossing_wall`, and a travel's time is not proportional to
|
||||||
|
its length: a short travel is bound by acceleration, so its time grows about
|
||||||
|
with the square root of its length. A chain that is shorter in total but trades
|
||||||
|
very short travels for longer ones, or for ones past `retraction_minimum_travel`,
|
||||||
|
can occasionally take a little longer to print.
|
||||||
|
|
||||||
|
The end of the chain is free: it is not steered toward the next island or the
|
||||||
|
start of the next layer. The regions of an island are chained one after the
|
||||||
|
other, in region order, each from where the previous one ended.
|
||||||
@@ -36839,7 +36839,9 @@ msgstr ""
|
|||||||
msgid ""
|
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.
@@ -0,0 +1,126 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Fails when a Windows build has an exception class with broken throw info.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Clang on the MSVC ABI can leave the copy constructor out of the CatchableType it emits for
|
||||||
|
an exception class that comes through a precompiled header
|
||||||
|
(https://github.com/llvm/llvm-project/issues/53486). The linker keeps one throw info per
|
||||||
|
class, so when it keeps such a record, a copy of that exception (std::exception_ptr, a catch
|
||||||
|
by value) shares its data with the original and the data is freed twice.
|
||||||
|
|
||||||
|
A class is either trivially copyable or not, so a binary should never have both a CatchableType
|
||||||
|
without a copy constructor (_CT??_R0?AV<class>@@@8<size>) and one with it
|
||||||
|
(_CT??_R0?AV<class>@@@8??0<class>...). The script lists each PDB's public symbols with the
|
||||||
|
llvm-pdbutil next to the build's compiler and reports every class that has both.
|
||||||
|
|
||||||
|
scripts\test_check_exception_throw_info.ps1 tests it.
|
||||||
|
|
||||||
|
.PARAMETER BuildDir
|
||||||
|
The slicer build directory. Its CMakeCache.txt names the compiler, and its src\<Config>
|
||||||
|
holds the PDBs to check, which must include OrcaSlicer.pdb.
|
||||||
|
|
||||||
|
.PARAMETER Config
|
||||||
|
The configuration whose PDBs to check (default: Release).
|
||||||
|
|
||||||
|
.PARAMETER Pdb
|
||||||
|
PDBs to check instead of the ones in <BuildDir>\src\<Config>, as a list or comma-separated.
|
||||||
|
|
||||||
|
.PARAMETER Dump
|
||||||
|
Saved `llvm-pdbutil dump --publics` output to check instead of PDBs. Needs no build.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
scripts\check_exception_throw_info.ps1 -BuildDir build
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[string] $BuildDir,
|
||||||
|
[string] $Config = 'Release',
|
||||||
|
[string[]] $Pdb,
|
||||||
|
[string[]] $Dump
|
||||||
|
)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# powershell -File passes "a.pdb,b.pdb" as one string. A path that exists as given stays whole.
|
||||||
|
function Split-PathList([string[]] $values) {
|
||||||
|
@($values | Where-Object { $_ } | ForEach-Object {
|
||||||
|
if ((Test-Path -LiteralPath $_) -or $_ -notmatch ',') { $_ } else { $_ -split ',' | Where-Object { $_ } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$Dump = Split-PathList $Dump
|
||||||
|
if (-not $Dump) {
|
||||||
|
if (-not $BuildDir) {
|
||||||
|
throw 'Name the slicer build directory with -BuildDir.'
|
||||||
|
}
|
||||||
|
$cache = Join-Path $BuildDir 'CMakeCache.txt'
|
||||||
|
if (-not (Test-Path $cache)) {
|
||||||
|
throw "No CMakeCache.txt in $BuildDir. Name the slicer build directory."
|
||||||
|
}
|
||||||
|
$entry = Select-String -Path $cache -Pattern '^CMAKE_CXX_COMPILER:\w+=(.+)$' | Select-Object -First 1
|
||||||
|
if (-not $entry) {
|
||||||
|
throw "No CMAKE_CXX_COMPILER in $cache."
|
||||||
|
}
|
||||||
|
$compiler = $entry.Matches[0].Groups[1].Value
|
||||||
|
# A tree configured with a bare compiler name found it on PATH.
|
||||||
|
if (-not (Split-Path $compiler)) {
|
||||||
|
$found = Get-Command $compiler -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if (-not $found) {
|
||||||
|
throw "$compiler, the compiler in $cache, is not on PATH."
|
||||||
|
}
|
||||||
|
$compiler = $found.Source
|
||||||
|
}
|
||||||
|
$pdbutil = Join-Path (Split-Path $compiler) 'llvm-pdbutil.exe'
|
||||||
|
if (-not (Test-Path $pdbutil)) {
|
||||||
|
throw "No llvm-pdbutil.exe next to $compiler. This check needs a build made with Clang."
|
||||||
|
}
|
||||||
|
|
||||||
|
$Pdb = Split-PathList $Pdb
|
||||||
|
if (-not $Pdb) {
|
||||||
|
# OrcaSlicer.dll holds the slicer itself, so a check without it proves nothing.
|
||||||
|
$pdbDir = Join-Path $BuildDir "src\$Config"
|
||||||
|
if (-not (Test-Path (Join-Path $pdbDir 'OrcaSlicer.pdb'))) {
|
||||||
|
throw "No OrcaSlicer.pdb in $pdbDir."
|
||||||
|
}
|
||||||
|
$Pdb = @(Get-ChildItem $pdbDir -Filter *.pdb | ForEach-Object FullName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$catchable = [regex]'`_CT\?\?_R0\?A[UV](.+?)@@@8([^`]*)`'
|
||||||
|
$tmp = [IO.Path]::GetTempFileName()
|
||||||
|
$failed = $false
|
||||||
|
$classes = 0
|
||||||
|
try {
|
||||||
|
foreach ($file in $(if ($Dump) { $Dump } else { $Pdb })) {
|
||||||
|
$text = $file
|
||||||
|
if (-not $Dump) {
|
||||||
|
$run = Start-Process -FilePath $pdbutil -ArgumentList 'dump', '--publics', "`"$file`"" -NoNewWindow -Wait -PassThru -RedirectStandardOutput $tmp
|
||||||
|
if ($run.ExitCode -ne 0) {
|
||||||
|
throw "llvm-pdbutil failed on $file."
|
||||||
|
}
|
||||||
|
$text = $tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
$kinds = @{}
|
||||||
|
foreach ($match in $catchable.Matches([IO.File]::ReadAllText((Resolve-Path -LiteralPath $text).Path))) {
|
||||||
|
$class = $match.Groups[1].Value
|
||||||
|
if (-not $kinds.ContainsKey($class)) { $kinds[$class] = @{} }
|
||||||
|
$kinds[$class][$(if ($match.Groups[2].Value.StartsWith('??0')) { 'copy' } else { 'bitwise' })] = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
$broken = @($kinds.Keys | Where-Object { $kinds[$_].Count -eq 2 } | Sort-Object)
|
||||||
|
"$(Split-Path -Leaf $file): $($kinds.Count) exception classes, $($broken.Count) with broken throw info"
|
||||||
|
foreach ($class in $broken) { " $class" }
|
||||||
|
if ($broken.Count -gt 0) { $failed = $true }
|
||||||
|
$classes += $kinds.Count
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
Remove-Item $tmp -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# The slicer's PDBs always contain exception throw info, so finding none means the dump format or the PDBs changed.
|
||||||
|
if ($classes -eq 0) {
|
||||||
|
'No exception classes found. Check that llvm-pdbutil still prints public symbols in backticks.'
|
||||||
|
$failed = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
exit [int]$failed
|
||||||
@@ -301,6 +301,10 @@ $cases = @(
|
|||||||
Contains = @('Precompiled header: off') }
|
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)
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+37
-37
@@ -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 ¢er) const
|
Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d ¢er) 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 ¢er) 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 ¶ms, coordf_t spacing,
|
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, 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);
|
||||||
+12
-12
@@ -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 ¶ms, coordf_t spacing,
|
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, 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
@@ -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
@@ -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
|
||||||
@@ -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_
|
||||||
@@ -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 = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
auto frequency = [¶ms, 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.
|
||||||
|
|||||||
@@ -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 = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
auto frequency = [¶ms, 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.
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = [¢ers](size_t a, size_t b) { return (centers[a].cast<double>() - centers[b].cast<double>()).norm(); };
|
||||||
|
for (size_t j = 0; j < m; ++j)
|
||||||
|
cost[(size_t(1) << j) * m + j] = dist(0, j + 1);
|
||||||
|
for (size_t mask = 1; mask < (size_t(1) << m); ++mask)
|
||||||
|
for (size_t j = 0; j < m; ++j)
|
||||||
|
if (const double c = cost[mask * m + j]; c < std::numeric_limits<double>::max())
|
||||||
|
for (size_t k = 0; k < m; ++k)
|
||||||
|
if (!(mask >> k & 1)) {
|
||||||
|
const size_t next = (mask | size_t(1) << k) * m + k;
|
||||||
|
if (const double nc = c + dist(j + 1, k + 1); nc < cost[next]) {
|
||||||
|
cost[next] = nc;
|
||||||
|
from[next] = mask * m + j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const size_t full = (size_t(1) << m) - 1;
|
||||||
|
size_t best = full * m;
|
||||||
|
for (size_t j = 1; j < m; ++j)
|
||||||
|
if (cost[full * m + j] + dist(j + 1, 0) < cost[best] + dist(best % m + 1, 0))
|
||||||
|
best = full * m + j;
|
||||||
|
for (size_t s = best, k = n; k-- > 1; s = from[s])
|
||||||
|
path[k] = s % m + 1;
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 |
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
+42
-42
@@ -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",
|
|||||||
[¢er](const ExPolygon &expolygon) { return expolygon.contains(center); }));
|
[¢er](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));
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/generators/catch_generators.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers.hpp>
|
||||||
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/ExtrusionEntity.hpp"
|
||||||
|
#include "libslic3r/ExtrusionEntityCollection.hpp"
|
||||||
|
#include "libslic3r/Point.hpp"
|
||||||
|
#include "libslic3r/Polyline.hpp"
|
||||||
|
#include "libslic3r/ShortestPath.hpp"
|
||||||
|
#include "libslic3r/libslic3r.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <numeric>
|
||||||
|
#include <math.h>
|
||||||
|
#include <random>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Infill-like paths: lines of 1 to 30 mm scattered over a 100 mm square. The collection owns them, and entities hands
|
||||||
|
// them out as the raw pointers chain_and_reorder_extrusion_entities() reorders.
|
||||||
|
struct Paths
|
||||||
|
{
|
||||||
|
ExtrusionEntityCollection collection;
|
||||||
|
std::vector<ExtrusionEntity*> entities;
|
||||||
|
|
||||||
|
Paths(unsigned seed, size_t count, bool can_reverse)
|
||||||
|
{
|
||||||
|
std::mt19937 rng(seed);
|
||||||
|
std::uniform_real_distribution<> coordinate(0., 100.), length(1., 30.), angle(0., 2. * M_PI);
|
||||||
|
Polylines lines;
|
||||||
|
for (size_t i = 0; i < count; ++ i) {
|
||||||
|
// One draw per statement, as the evaluation order of function arguments is unspecified.
|
||||||
|
const double x = coordinate(rng);
|
||||||
|
const double y = coordinate(rng);
|
||||||
|
const double l = length(rng);
|
||||||
|
const double t = angle(rng);
|
||||||
|
const Vec2d a(x, y);
|
||||||
|
const Vec2d b = a + l * Vec2d(std::cos(t), std::sin(t));
|
||||||
|
lines.emplace_back(Point::new_scale(a), Point::new_scale(b));
|
||||||
|
}
|
||||||
|
extrusion_entities_append_paths(collection.entities, std::move(lines), erInternalInfill, 0.1, 0.45f, 0.2f, can_reverse);
|
||||||
|
entities = collection.entities;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
double travel(const std::vector<ExtrusionEntity*> &entities, const Point &start)
|
||||||
|
{
|
||||||
|
double length = 0.;
|
||||||
|
Point last = start;
|
||||||
|
for (const ExtrusionEntity *ee : entities) {
|
||||||
|
length += (ee->first_point() - last).cast<double>().norm();
|
||||||
|
last = ee->last_point();
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Travel of the chain that always goes on to the nearest free path end.
|
||||||
|
double nearest_next_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
||||||
|
{
|
||||||
|
std::vector<char> taken(entities.size(), 0);
|
||||||
|
double length = 0.;
|
||||||
|
Point last = from;
|
||||||
|
for (size_t step = 0; step < entities.size(); ++ step) {
|
||||||
|
double best = std::numeric_limits<double>::max();
|
||||||
|
size_t best_idx = 0;
|
||||||
|
bool best_reversed = false;
|
||||||
|
for (size_t i = 0; i < entities.size(); ++ i)
|
||||||
|
for (const bool reversed : { false, true })
|
||||||
|
if (! taken[i] && (can_reverse || ! reversed)) {
|
||||||
|
const double d = ((reversed ? entities[i]->last_point() : entities[i]->first_point()) - last).cast<double>().norm();
|
||||||
|
if (d < best) {
|
||||||
|
best = d;
|
||||||
|
best_idx = i;
|
||||||
|
best_reversed = reversed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taken[best_idx] = 1;
|
||||||
|
length += best;
|
||||||
|
last = best_reversed ? entities[best_idx]->first_point() : entities[best_idx]->last_point();
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shortest travel over every order and allowed reversal of the paths, from start.
|
||||||
|
double shortest_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
||||||
|
{
|
||||||
|
std::vector<size_t> order(entities.size());
|
||||||
|
std::iota(order.begin(), order.end(), 0);
|
||||||
|
double best = std::numeric_limits<double>::max();
|
||||||
|
do {
|
||||||
|
for (size_t mask = 0; mask < (size_t(1) << order.size()) && (can_reverse || mask == 0); ++ mask) {
|
||||||
|
double length = 0.;
|
||||||
|
Point last = from;
|
||||||
|
for (size_t k = 0; k < order.size(); ++ k) {
|
||||||
|
const ExtrusionEntity *ee = entities[order[k]];
|
||||||
|
const bool reversed = (mask >> k & 1) != 0;
|
||||||
|
length += ((reversed ? ee->last_point() : ee->first_point()) - last).cast<double>().norm();
|
||||||
|
last = reversed ? ee->first_point() : ee->last_point();
|
||||||
|
}
|
||||||
|
best = std::min(best, length);
|
||||||
|
}
|
||||||
|
} while (std::next_permutation(order.begin(), order.end()));
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Point start = Point::new_scale(-10., -10.);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("An improved infill chain is never longer than the greedy or nearest-next one and keeps every path", "[ShortestPath]")
|
||||||
|
{
|
||||||
|
const size_t count = GENERATE(2, 3, 10, 60, 300);
|
||||||
|
const bool can_reverse = GENERATE(true, false);
|
||||||
|
double greedy_total = 0., improved_total = 0.;
|
||||||
|
for (unsigned seed = 1; seed <= 20; ++ seed) {
|
||||||
|
Paths greedy(seed, count, can_reverse), improved(seed, count, can_reverse);
|
||||||
|
const double nearest_travel = nearest_next_travel(improved.entities, can_reverse, start);
|
||||||
|
chain_and_reorder_extrusion_entities(greedy.entities, &start);
|
||||||
|
chain_and_reorder_extrusion_entities(improved.entities, start);
|
||||||
|
|
||||||
|
std::vector<ExtrusionEntity*> sorted = improved.entities;
|
||||||
|
std::sort(sorted.begin(), sorted.end());
|
||||||
|
REQUIRE(std::unique(sorted.begin(), sorted.end()) == sorted.end());
|
||||||
|
REQUIRE(sorted.size() == count);
|
||||||
|
|
||||||
|
const double greedy_travel = travel(greedy.entities, start), improved_travel = travel(improved.entities, start);
|
||||||
|
CHECK(improved_travel <= greedy_travel + SCALED_EPSILON);
|
||||||
|
CHECK(improved_travel <= nearest_travel + SCALED_EPSILON);
|
||||||
|
greedy_total += greedy_travel;
|
||||||
|
improved_total += improved_travel;
|
||||||
|
}
|
||||||
|
// Twenty random sets of ten or more paths always leave the greedy chain something to shorten.
|
||||||
|
if (count >= 10)
|
||||||
|
CHECK(improved_total < 0.95 * greedy_total);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Improving an infill chain keeps the direction of paths that cannot reverse", "[ShortestPath]")
|
||||||
|
{
|
||||||
|
Paths paths(7, 100, false);
|
||||||
|
std::map<const ExtrusionEntity*, Point> first_points;
|
||||||
|
for (const ExtrusionEntity *ee : paths.entities)
|
||||||
|
first_points.emplace(ee, ee->first_point());
|
||||||
|
|
||||||
|
chain_and_reorder_extrusion_entities(paths.entities, start);
|
||||||
|
|
||||||
|
for (const ExtrusionEntity *ee : paths.entities)
|
||||||
|
CHECK(ee->first_point() == first_points.at(ee));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Chains of a few paths are the shortest possible", "[ShortestPath]")
|
||||||
|
{
|
||||||
|
const size_t count = GENERATE(2, 3, 4, 5, 6);
|
||||||
|
const bool can_reverse = GENERATE(true, false);
|
||||||
|
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
||||||
|
Paths paths(seed, count, can_reverse);
|
||||||
|
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
||||||
|
chain_and_reorder_extrusion_entities(paths.entities, start);
|
||||||
|
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The greedy chain of two to four paths is the shortest possible", "[ShortestPath]")
|
||||||
|
{
|
||||||
|
const size_t count = GENERATE(2, 3, 4);
|
||||||
|
const bool can_reverse = GENERATE(true, false);
|
||||||
|
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
||||||
|
Paths paths(seed, count, can_reverse);
|
||||||
|
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
||||||
|
chain_and_reorder_extrusion_entities(paths.entities, &start);
|
||||||
|
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A short chain need not start with the path nearest to its start", "[ShortestPath]")
|
||||||
|
{
|
||||||
|
// The nearest end leads 10 mm away from the two other paths, so the shortest chain leaves that path for last.
|
||||||
|
const Point from = Point::new_scale(0., 0.);
|
||||||
|
for (const bool greedy : { false, true }) {
|
||||||
|
ExtrusionEntityCollection collection;
|
||||||
|
extrusion_entities_append_paths(collection.entities,
|
||||||
|
Polylines{ Polyline(Point::new_scale(-0.9, 0.), Point::new_scale(-10., 0.)), Polyline(Point::new_scale(1., 0.), Point::new_scale(2., 0.)),
|
||||||
|
Polyline(Point::new_scale(3., 1.), Point::new_scale(4., 1.)) },
|
||||||
|
erInternalInfill, 0.1, 0.45f, 0.2f);
|
||||||
|
std::vector<ExtrusionEntity*> entities = collection.entities;
|
||||||
|
const double shortest = shortest_travel(entities, true, from);
|
||||||
|
if (greedy)
|
||||||
|
chain_and_reorder_extrusion_entities(entities, &from);
|
||||||
|
else
|
||||||
|
chain_and_reorder_extrusion_entities(entities, from);
|
||||||
|
CHECK(entities.back() == collection.entities.front());
|
||||||
|
CHECK_THAT(travel(entities, from), Catch::Matchers::WithinAbs(shortest, SCALED_EPSILON));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#include <catch2/catch_message.hpp>
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/Model.hpp"
|
||||||
|
#include "libslic3r/TexturePainting.hpp"
|
||||||
|
#include "texture_import_cgal.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void check_texture_import(const std::string& fixture)
|
||||||
|
{
|
||||||
|
const std::string filename = std::string(TEST_DATA_DIR) + "/texture_import/" + fixture;
|
||||||
|
INFO(filename);
|
||||||
|
const Slic3r::Model model = Slic3r::Model::read_from_file(filename);
|
||||||
|
REQUIRE_FALSE(model.objects.empty());
|
||||||
|
REQUIRE(model.texture_mesh);
|
||||||
|
const Slic3r::TexturedMesh& mesh = *model.texture_mesh;
|
||||||
|
|
||||||
|
Slic3r::TexturePaintingSettings settings;
|
||||||
|
settings.target_colors_num = 0; // Match the dialog's initial automatic color count.
|
||||||
|
settings.smooth_weight = 0.5;
|
||||||
|
settings.mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
|
||||||
|
Slic3r::PaintedMesh painted;
|
||||||
|
// Follow the dialog worker's format-dependent path, including the boundary
|
||||||
|
// smoothing that needs a consistent CGAL Kd_tree layout across libraries.
|
||||||
|
if (!mesh.precomputed_face_colors.empty())
|
||||||
|
REQUIRE(Slic3r::face_colors_to_painting(mesh, painted, settings));
|
||||||
|
else
|
||||||
|
REQUIRE(Slic3r::texture_to_painting(mesh, painted, settings));
|
||||||
|
|
||||||
|
REQUIRE_FALSE(painted.indices.empty());
|
||||||
|
REQUIRE(painted.face_colors.size() == painted.indices.size());
|
||||||
|
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
|
||||||
|
[](const auto& color) { return color[0] > color[2]; }));
|
||||||
|
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
|
||||||
|
[](const auto& color) { return color[2] > color[0]; }));
|
||||||
|
|
||||||
|
const std::vector<std::array<float, 4>> filament_colors{
|
||||||
|
{1.f, 0.f, 0.f, 1.f}, {0.f, 0.f, 1.f, 1.f}};
|
||||||
|
const auto matches = Slic3r::match_clusters_to_filaments(
|
||||||
|
painted.cluster_colors, filament_colors, {"Red PLA", "Blue PLA"});
|
||||||
|
REQUIRE(matches.size() == painted.cluster_colors.size());
|
||||||
|
REQUIRE(std::any_of(matches.begin(), matches.end(),
|
||||||
|
[](const auto& match) { return match.filament_index == 0; }));
|
||||||
|
REQUIRE(std::any_of(matches.begin(), matches.end(),
|
||||||
|
[](const auto& match) { return match.filament_index == 1; }));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("Color-boundary smoothing shares its CGAL tree layout with the geometry backend", "[TextureImport][CGAL][Regression]")
|
||||||
|
{
|
||||||
|
// An ABI mismatch need not crash every link of the importer. Compare the
|
||||||
|
// layouts directly so a lucky choice of inline symbols cannot mask it.
|
||||||
|
REQUIRE(sizeof(TextureImportTest::SearchTree) == TextureImportTest::backend_search_tree_size());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A JPEG textured GLB retains both colors through filament matching", "[TextureImport][Regression]")
|
||||||
|
{
|
||||||
|
check_texture_import("two_color_jpeg.glb");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An OBJ preserves both MTL colors through filament matching", "[TextureImport][OBJ][Regression]")
|
||||||
|
{
|
||||||
|
check_texture_import("two_color_sphere/test_two_color_sphere.obj");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An OBJ resolves its MTL and external JPEG through filament matching", "[TextureImport][OBJ][Regression]")
|
||||||
|
{
|
||||||
|
check_texture_import("two_color_sphere/test_two_color_sphere_textured.obj");
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#include "texture_import_cgal.hpp"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
std::size_t TextureImportTest::backend_search_tree_size()
|
||||||
|
{
|
||||||
|
return sizeof(SearchTree);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <CGAL/AABB_face_graph_triangle_primitive.h>
|
||||||
|
#include <CGAL/AABB_traits_3.h>
|
||||||
|
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
|
||||||
|
#include <CGAL/Kd_tree.h>
|
||||||
|
#include <CGAL/Search_traits_adapter.h>
|
||||||
|
#include <CGAL/Surface_mesh/Surface_mesh.h>
|
||||||
|
#include <CGAL/property_map.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace TextureImportTest {
|
||||||
|
|
||||||
|
// The distance-query tree used by ColorUtils.cpp's boundary smoothing. Its
|
||||||
|
// type name stays the same when CGAL changes its storage for TBB support.
|
||||||
|
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
|
||||||
|
using Mesh = CGAL::Surface_mesh<Kernel::Point_3>;
|
||||||
|
using Primitive = CGAL::AABB_face_graph_triangle_primitive<Mesh>;
|
||||||
|
using Traits = CGAL::AABB_traits_3<Kernel, Primitive>;
|
||||||
|
using PointId = Traits::Point_and_primitive_id;
|
||||||
|
using SearchTraits = CGAL::Search_traits_adapter<PointId, CGAL::First_of_pair_property_map<PointId>, Traits>;
|
||||||
|
using SearchTree = CGAL::Kd_tree<SearchTraits>;
|
||||||
|
|
||||||
|
// Implemented inside the actual geometry target, using its private definitions.
|
||||||
|
std::size_t backend_search_tree_size();
|
||||||
|
|
||||||
|
} // namespace TextureImportTest
|
||||||
@@ -44,6 +44,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_shortcuts.cpp
|
test_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]")
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/GCode/ThumbnailData.hpp"
|
||||||
|
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||||
|
#include "libslic3r/PrintConfig.hpp"
|
||||||
|
#include "libslic3r/TexturePainting.hpp"
|
||||||
|
#include "slic3r/GUI/AVVideoDecoder.hpp"
|
||||||
|
#include "slic3r/GUI/Printer/BambuTunnel.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
#include <wx/bitmap.h>
|
||||||
|
#include <wx/gdicmn.h>
|
||||||
|
#include <wx/image.h>
|
||||||
|
#include <wx/init.h>
|
||||||
|
#include <wx/mstream.h>
|
||||||
|
|
||||||
|
TEST_CASE("JPEG texture decoding coexists with thumbnails and video frames", "[TimelapseCodecs][Regression]")
|
||||||
|
{
|
||||||
|
#if defined(__linux__)
|
||||||
|
if (!std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY"))
|
||||||
|
SKIP("This integration test needs a display (for example, xvfb-run).");
|
||||||
|
#endif
|
||||||
|
wxInitializer init;
|
||||||
|
REQUIRE(init.IsOk());
|
||||||
|
wxInitAllImageHandlers();
|
||||||
|
|
||||||
|
// Generate a JPEG through the same encoder used for G-code thumbnails.
|
||||||
|
constexpr int width = 160, height = 90, iterations = 1000;
|
||||||
|
Slic3r::ThumbnailData thumbnail;
|
||||||
|
thumbnail.set(width, height);
|
||||||
|
for (std::size_t i = 0; i < thumbnail.pixels.size(); i += 4) {
|
||||||
|
thumbnail.pixels[i] = 180;
|
||||||
|
thumbnail.pixels[i + 1] = 70;
|
||||||
|
thumbnail.pixels[i + 2] = 30;
|
||||||
|
thumbnail.pixels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
const auto jpeg = Slic3r::GCodeThumbnails::compress_thumbnail(thumbnail, Slic3r::GCodeThumbnailsFormat::JPG);
|
||||||
|
REQUIRE(jpeg);
|
||||||
|
REQUIRE(jpeg->size > 0);
|
||||||
|
const auto* bytes = static_cast<const unsigned char*>(jpeg->data);
|
||||||
|
Slic3r::TextureImage texture;
|
||||||
|
texture.width = texture.height = -1;
|
||||||
|
texture.data.assign(bytes, bytes + jpeg->size);
|
||||||
|
|
||||||
|
std::atomic<bool> texture_ok{true};
|
||||||
|
std::thread texture_worker([&] {
|
||||||
|
for (int i = 0; i < iterations; ++i) {
|
||||||
|
std::vector<unsigned char> pixels;
|
||||||
|
int decoded_width = 0, decoded_height = 0;
|
||||||
|
if (!Slic3r::decode_texture_to_pixels(texture, pixels, decoded_width, decoded_height) ||
|
||||||
|
decoded_width != width || decoded_height != height || pixels.size() != width * height * 3)
|
||||||
|
texture_ok = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PrinterFileSystem decodes timelapse thumbnails with wxImage. Do not
|
||||||
|
// assert until the worker is joined: Catch assertions can unwind the scope.
|
||||||
|
bool thumbnails_ok = true;
|
||||||
|
for (int i = 0; i < iterations; ++i) {
|
||||||
|
wxMemoryInputStream stream(jpeg->data, jpeg->size);
|
||||||
|
wxImage image(stream, "image/jpeg");
|
||||||
|
if (!image.IsOk() || image.GetWidth() != width || image.GetHeight() != height ||
|
||||||
|
image.GetRed(width / 2, height / 2) < 170 || image.GetGreen(width / 2, height / 2) > 80)
|
||||||
|
thumbnails_ok = false;
|
||||||
|
}
|
||||||
|
texture_worker.join();
|
||||||
|
REQUIRE(texture_ok.load());
|
||||||
|
REQUIRE(thumbnails_ok);
|
||||||
|
|
||||||
|
// Exercise Orca's real FFmpeg decoder and its platform display conversion.
|
||||||
|
AVVideoDecoder decoder;
|
||||||
|
Bambu_StreamInfo info{};
|
||||||
|
info.type = VIDE;
|
||||||
|
info.sub_type = MJPG;
|
||||||
|
info.format_type = video_jpeg;
|
||||||
|
REQUIRE(decoder.open(info) == 0);
|
||||||
|
Bambu_Sample sample{};
|
||||||
|
sample.size = static_cast<int>(jpeg->size);
|
||||||
|
sample.buffer = bytes;
|
||||||
|
for (int i = 0; i < iterations; ++i) {
|
||||||
|
REQUIRE(decoder.decode(sample) == 0);
|
||||||
|
wxImage image;
|
||||||
|
REQUIRE(decoder.toWxImage(image, wxSize(width, height)));
|
||||||
|
REQUIRE(image.IsOk());
|
||||||
|
REQUIRE(image.GetWidth() == width);
|
||||||
|
REQUIRE(image.GetHeight() == height);
|
||||||
|
#ifdef _WIN32
|
||||||
|
// Playback uses this raw 32-bit bitmap conversion only on Windows.
|
||||||
|
wxBitmap bitmap;
|
||||||
|
REQUIRE(decoder.toWxBitmap(bitmap, wxSize(width, height)));
|
||||||
|
REQUIRE(bitmap.IsOk());
|
||||||
|
REQUIRE(bitmap.GetWidth() == width);
|
||||||
|
REQUIRE(bitmap.GetHeight() == height);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user