From f92bd811904b6223241fd967c54cfa1554e6f017 Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Thu, 3 Sep 2026 07:56:33 -0500 Subject: [PATCH 1/6] fix: build_win.bat builds with whatever clang-cl is first on PATH (#15504) * fix: build_win.bat builds with whatever clang-cl is first on PATH VsDevCmd appends the Visual Studio LLVM directory to the end of PATH, so a standalone LLVM already on it shadows the Visual Studio one. -l -x passed a bare clang-cl.exe for CMake to resolve, so the build ran on whichever copy came first. For one reporter that was an LLVM 11, which failed the compiler check before anything was compiled: -- Check for working C compiler: C:/Program Files/LLVM/bin/clang-cl.exe - broken lld-link: error: undefined symbol: __guard_eh_cont_table The compiler is now resolved through vswhere and passed as a full path, so PATH order no longer matters. CMake derives the linker from the compiler directory, so lld-link follows. Only a configure passes it to CMake, so -p and --no-configure resolve nothing and stay buildable on a machine with no clang installed. When Visual Studio has no clang toolset the script falls back to the first clang-cl on PATH and names it. With none installed at all it now errors with what to add, instead of failing later inside CMake. Every clang-cl run that configures prints the compiler it resolved. The suite gains a clang-cl fixture earlier on PATH than the Visual Studio one, and an empty ProgramFiles(x86) to put vswhere out of reach, which covers both fallbacks without touching the machine. * fix: build_win.bat pointed at a solution file that is not there The Visual Studio 2026 generator writes OrcaSlicer.slnx and the releases before it OrcaSlicer.sln. The summary hard-coded the second, so the path it printed after an MSVC build against 2026 was wrong. --- build_win.bat | 79 +++++++++++++++++++++++++++++++++---- scripts/test_build_win.ps1 | 80 +++++++++++++++++++++++++++++++++----- 2 files changed, 142 insertions(+), 17 deletions(-) diff --git a/build_win.bat b/build_win.bat index 926468687b..15399d5e3d 100644 --- a/build_win.bat +++ b/build_win.bat @@ -370,13 +370,13 @@ if "%use_ninja%" == "ON" ( set "using_ninja=ON" ) +call :resolve_clang_cl +%repeat_error% + if "%using_ninja%" == "ON" ( if "%use_clang_cl%" == "ON" ( - REM Bare, so it resolves from the PATH the dev shell just set up, which - REM is the clang shipped with Visual Studio. --clang-path names another. - set "clang_exe=clang-cl.exe" - if not "%clang_path%" == "" set "clang_exe="%clang_path%"" - set "gen_args=-DCMAKE_C_COMPILER=!clang_exe! -DCMAKE_CXX_COMPILER=!clang_exe!" + REM Quoted, because the resolved path has spaces in it. + set "gen_args=-DCMAKE_C_COMPILER="!clang_exe!" -DCMAKE_CXX_COMPILER="!clang_exe!"" ) ) else ( set "gen_args=-A !arch!" @@ -473,6 +473,7 @@ REM forward slashes, and anything that prints the directory has to show a REM real path rather than one glued onto the repository root. for %%p in ("!build_dir!") do set "build_full=%%~fp" echo Configuration: %build_type%, %arch% +if not "%clang_exe%" == "" echo Compiler: %clang_exe% set "SIG_FLAG=" if defined ORCA_UPDATER_SIG_KEY set "SIG_FLAG=-DORCA_UPDATER_SIG_KEY=%ORCA_UPDATER_SIG_KEY%" @@ -734,6 +735,14 @@ REM worked out the same way in either run. set "slicer_exe=%build_dir%\src\%build_type%\orca-slicer.exe" if "%install_slicer%" == "ON" set "slicer_exe=%build_dir%\OrcaSlicer\orca-slicer.exe" for %%p in ("!slicer_exe!") do set "slicer_full=%%~fp" + REM The 2026 generator writes OrcaSlicer.slnx, the releases before it + REM OrcaSlicer.sln. A file already there wins, in case an older CMake + REM configured the build. + set "solution=OrcaSlicer.sln" + if "%vs_version%" == "2026" set "solution=OrcaSlicer.slnx" + if exist "!build_full!\OrcaSlicer.sln" set "solution=OrcaSlicer.sln" + if exist "!build_full!\OrcaSlicer.slnx" set "solution=OrcaSlicer.slnx" + REM Naming a target builds it and its dependencies, not its dependents, REM so only a full build or the executable's own target relinks. set "linked=ON" @@ -751,14 +760,14 @@ REM worked out the same way in either run. if "%build_deps%" == "ON" echo Dependencies !dep_full! if "%build_slicer%" == "ON" if "%linked%" == "ON" echo OrcaSlicer !slicer_full! if "%build_slicer%" == "ON" if not "%linked%" == "ON" echo Target %slicer_target% - if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\OrcaSlicer.sln + if "%build_slicer%" == "ON" if not "%using_ninja%" == "ON" echo Solution %build_full%\!solution! if "%pack_deps%" == "ON" if defined bundle echo Bundle !bundle! echo. echo Next if "%build_slicer%" == "ON" ( if "%linked%" == "ON" echo Run it !slicer_exe! - if not "%using_ninja%" == "ON" echo Open in Visual Studio %build_dir%\OrcaSlicer.sln + if not "%using_ninja%" == "ON" echo Open in Visual Studio %build_dir%\!solution! if "%linked%" == "ON" echo Rebuild after edits build_win.bat -s!recall! --no-configure if not "%linked%" == "ON" echo Relink the binary build_win.bat -s!recall! --no-configure if "%linked%" == "ON" if "%using_ninja%" == "ON" echo Rebuild one target build_win.bat -s!recall! --no-configure --slicer-target libslic3r @@ -1080,6 +1089,62 @@ REM echo_var exit /b 0 +REM resolve_clang_cl - set clang_exe to the clang-cl a Ninja build uses. +REM VsDevCmd appends the Visual Studio LLVM directory to the end of PATH, +REM so a standalone LLVM already there shadows it. Name a full path. +:resolve_clang_cl + set "clang_exe=" + if not "%using_ninja%" == "ON" exit /b 0 + if not "%use_clang_cl%" == "ON" exit /b 0 + REM Only a configure uses it, so -p and --no-configure need none. + if "%no_configure%" == "ON" exit /b 0 + if "%build_deps%%build_slicer%" == "" exit /b 0 + + REM --clang-path wins. Forward slashes either way, so CMake does not + REM read a backslash as an escape. + if not "%clang_path%" == "" ( + set "clang_exe=%clang_path:\=/%" + exit /b 0 + ) + + setlocal + + REM Keyed on the host, not %arch%, because the x64 compiler + REM cross-compiles to ARM64. + set "llvm_host=x64" + if /I "%PROCESSOR_ARCHITECTURE%" == "ARM64" set "llvm_host=ARM64" + + set "found=" + %VSWHERE% -nologo >nul 2>nul + if !errorlevel! == 0 ( + for /f "tokens=*" %%i in ('%VSWHERE% -nologo -products * -latest -property resolvedInstallationPath') do ( + if exist "%%i\VC\Tools\Llvm\!llvm_host!\bin\clang-cl.exe" ( + set "found=%%i\VC\Tools\Llvm\!llvm_host!\bin\clang-cl.exe" + ) + ) + ) + + REM No clang toolset in Visual Studio. where lists every match, and the + REM first is the one PATH would resolve. + if "!found!" == "" ( + for /f "tokens=*" %%i in ('where clang-cl.exe 2^>nul') do ( + if "!found!" == "" set "found=%%i" + ) + if not "!found!" == "" echo Visual Studio has no clang-cl; using !found! from PATH. + ) + + if "!found!" == "" ( + echo No clang-cl found. Add the C++ Clang Compiler component with + echo %script_name% --install-vs ide -l + echo or name a standalone one with --clang-path. + endlocal + exit /b 1 + ) + + endlocal & set "clang_exe=%found:\=/%" + + exit /b 0 + REM clean_tree - remove a build tree, refusing anything that is not REM one. Nothing here should ever fire; it is a floor under a bug that REM produced a path far shorter than it looks. diff --git a/scripts/test_build_win.ps1 b/scripts/test_build_win.ps1 index 62eaaae77b..563430559f 100644 --- a/scripts/test_build_win.ps1 +++ b/scripts/test_build_win.ps1 @@ -85,6 +85,24 @@ foreach ($v in @{ old = '1.11.1'; new = '1.12.0' }.GetEnumerator()) { $ninjaPaths[$v.Key] = "$d;$env:PATH" } +# A clang-cl earlier on PATH than the Visual Studio one, which is what the +# compiler used to resolve to. Nothing runs it; the script only locates it. +$clangDir = Join-Path $fixtures 'clang' +New-Item -ItemType Directory -Force -Path $clangDir | Out-Null +Copy-Item "$env:SystemRoot\System32\where.exe" (Join-Path $clangDir 'clang-cl.exe') -Force +$clangOnPath = "$clangDir;$env:PATH" + +# ProgramFiles(x86) is where the script looks for vswhere, so an empty one +# stands in for a machine whose Visual Studio has no clang toolset. +$noVs = Join-Path $fixtures 'no-vs' +New-Item -ItemType Directory -Force -Path $noVs | Out-Null + +# A build directory that already holds a classic solution, for the case where +# what is on disk disagrees with what the generator would write. +$slnDir = Join-Path $fixtures 'sln' +New-Item -ItemType Directory -Force -Path $slnDir | Out-Null +Set-Content -Path (Join-Path $slnDir 'OrcaSlicer.sln') -Value '' -Encoding ascii + # The pack stamp is checked against real dates, so a locale-dependent parse # in the script cannot pass by looking date-shaped. Yesterday is accepted too, # so a run that crosses midnight does not flake. @@ -131,7 +149,36 @@ $cases = @( Contains = @('-G "Ninja Multi-Config"') NotContains = @('clang-cl', '-A x64') } @{ Name = '-l -x builds with clang-cl under Ninja'; Args = @('-d', '-l', '-x') - Contains = @('-G "Ninja Multi-Config"', '-DCMAKE_C_COMPILER=clang-cl.exe', '-DCMAKE_CXX_COMPILER=clang-cl.exe') } + Contains = @('-G "Ninja Multi-Config"') + Match = @('-DCMAKE_C_COMPILER="[^"]+/clang-cl\.exe"', '-DCMAKE_CXX_COMPILER="[^"]+/clang-cl\.exe"') } + # PATH order used to decide the compiler. VsDevCmd appends the Visual + # Studio LLVM directory to the end of PATH, so a standalone LLVM already + # there was resolved instead, and an old one failed the compiler check. + @{ Name = 'the compiler is resolved from Visual Studio, not PATH'; Args = @('-s', '-l', '-x') + Env = @{ PATH = $clangOnPath } + Match = @('^Compiler: .*/VC/Tools/Llvm/[^/]+/bin/clang-cl\.exe$') } + @{ Name = 'msvc names no compiler, having resolved none'; Args = @('-s') + NotContains = @('Compiler: ') } + @{ Name = '-l without -x names none either, the toolset picks it'; Args = @('-s', '-l') + NotContains = @('Compiler: ') } + # An empty ProgramFiles(x86) puts vswhere out of reach, which is a machine + # whose Visual Studio has no clang toolset. + @{ Name = 'without a Visual Studio clang the one on PATH is used and named'; Args = @('-s', '-l', '-x') + Env = @{ 'ProgramFiles(x86)' = $noVs; PATH = $clangOnPath } + Contains = @('Visual Studio has no clang-cl') + Match = @('^Compiler: .*/clang/clang-cl\.exe$') } + @{ Name = 'no clang-cl anywhere stops before configuring'; Args = @('-s', '-l', '-x'); ExpectExit = 1 + Env = @{ 'ProgramFiles(x86)' = $noVs; PATH = 'C:\Windows\system32;C:\Windows' } + Contains = @('No clang-cl found', '--install-vs ide -l') + NotContains = @('cmake -B') } + # Only a configure passes the compiler to CMake, so an action that does + # not configure resolves none, and cannot start needing one installed. + @{ Name = 'packing resolves no compiler'; Args = @('-p', '-l', '-x') + Contains = @('Packing the dependencies') + NotContains = @('Compiler: ') } + @{ Name = '--no-configure resolves none either'; Args = @('-s', '-l', '-x', '--no-configure') + Contains = @('cmake --build "build-clang"') + NotContains = @('Compiler: ') } @{ Name = '-l alone uses the ClangCL toolset on the VS generator'; Args = @('-d', '-l') Contains = @('-G "Visual Studio', '-T ClangCL') NotContains = @('-DCMAKE_C_COMPILER') } @@ -148,11 +195,13 @@ $cases = @( NotContains = @('clang-cl') } @{ Name = '--msbuild with -l gives the VS generator and the ClangCL toolset'; Args = @('-d', '--msbuild', '-l') Contains = @('-G "Visual Studio', '-T ClangCL') } - # A developer with a standalone LLVM points at it; the VS-bundled clang - # is what a bare clang-cl.exe resolves to after the dev shell runs. + # A developer with a standalone LLVM points at it, and the path is passed + # with forward slashes so CMake cannot read a backslash as an escape. @{ Name = '--clang-path names the compiler, quoted for its spaces'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') - Contains = @('-DCMAKE_C_COMPILER="C:\Program Files\LLVM\bin\clang-cl.exe"', - '-DCMAKE_CXX_COMPILER="C:\Program Files\LLVM\bin\clang-cl.exe"') } + Contains = @('-DCMAKE_C_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe"', + '-DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang-cl.exe"') } + @{ Name = '--clang-path beats the Visual Studio clang'; Args = @('-s', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') + Contains = @('Compiler: C:/Program Files/LLVM/bin/clang-cl.exe') } @{ Name = '--clang-path is a clang request on its own'; Args = @('-d', '-x', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe') Contains = @('deps/build-clang') } @{ Name = '--clang-path needs Ninja to take effect'; Args = @('-d', '--clang-path', 'C:\Program Files\LLVM\bin\clang-cl.exe'); ExpectExit = 1 @@ -195,7 +244,8 @@ $cases = @( @{ Name = 'the architecture is matched case-insensitively'; Args = @('-d', '--arch', 'ARM64') Contains = @('-A ARM64', 'deps/build-arm64') } @{ Name = 'arm64 under Ninja has no -A but keeps the arm64 tree'; Args = @('-d', '--arch', 'arm64', '-x', '-l') - Contains = @('deps/build-clang-arm64', '-DCMAKE_C_COMPILER=clang-cl.exe') + Contains = @('deps/build-clang-arm64') + Match = @('-DCMAKE_C_COMPILER="[^"]+/clang-cl\.exe"') NotContains = @('-A ') } 'build configurations' @@ -699,18 +749,28 @@ $cases = @( Contains = @('Next', 'Rebuild after edits') } 'pointing at the solution' + # The extension follows the generator, so these two pin the release and a + # build directory that cannot already hold a solution of either kind. + @{ Name = 'the 2026 generator gets the XML solution'; Args = @('-s', '--vs', '2026', '--build-dir', 'D:\tree') + Contains = @('Solution D:\tree\OrcaSlicer.slnx', 'Open in Visual Studio D:\tree\OrcaSlicer.slnx') } + @{ Name = 'the releases before it get the classic one'; Args = @('-s', '--vs', '2022', '--build-dir', 'D:\tree') + Contains = @('Solution D:\tree\OrcaSlicer.sln', 'Open in Visual Studio D:\tree\OrcaSlicer.sln') } + @{ Name = 'a solution already on disk wins over the generator'; Args = @('-s', '--vs', '2026', '--build-dir', $slnDir) + Match = @('^ Solution .*\\OrcaSlicer\.sln$') } + # Extension-agnostic from here: these cases are about the directory, and + # the release is whatever is installed. @{ Name = 'the VS generator says where the solution is'; Args = @('-s') - Match = @('^ Solution .*\\build\\OrcaSlicer\.sln$') } + Match = @('^ Solution .*\\build\\OrcaSlicer\.slnx?$') } @{ Name = 'the solution path follows the configuration'; Args = @('-s', '--config', 'debug') - Match = @('^ Solution .*\\build-dbg\\OrcaSlicer\.sln$') } + Match = @('^ Solution .*\\build-dbg\\OrcaSlicer\.slnx?$') } @{ Name = 'the solution line survives an install'; Args = @('-s', '-i') Contains = @(' Solution ') } # The path is resolved, not pasted onto the repository root, so it is # right whether --build-dir came absolute or with forward slashes. @{ Name = 'a moved build still prints one real path'; Args = @('-s', '--build-dir', 'out/build/x64-clang') - Match = @('^ Solution [A-Za-z]:\\[^/]+\\OrcaSlicer\.sln$') } + Match = @('^ Solution [A-Za-z]:\\[^/]+\\OrcaSlicer\.slnx?$') } @{ Name = 'an absolute --build-dir is not glued onto the repo root'; Args = @('-s', '--build-dir', 'D:\tree') - Contains = @('Solution D:\tree\OrcaSlicer.sln') } + Match = @('^ Solution D:\\tree\\OrcaSlicer\.slnx?$') } ) function Invoke-BuildScript { From 6a13cc2ab6a3a9ceb610aa5cfb481c881d6b3a7a Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:25:21 +0300 Subject: [PATCH 2/6] Fix Detach from parent checkbox not updating visually (#15520) Refresh detach-from-parent checkbox state Allow the detach checkbox toggle event to propagate to the custom CheckBox control so it refreshes its bitmap after the value changes. --- src/slic3r/GUI/SavePresetDialog.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/SavePresetDialog.cpp b/src/slic3r/GUI/SavePresetDialog.cpp index b3f34a8cb9..65b75c3d92 100644 --- a/src/slic3r/GUI/SavePresetDialog.cpp +++ b/src/slic3r/GUI/SavePresetDialog.cpp @@ -149,7 +149,10 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string &suffix, wxBox // Set initial state (unchecked by default) detach_checkbox->SetValue(m_detach); // Bind the checkbox event to update the detach state for this item - detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent&) { m_detach = detach_checkbox->GetValue(); }); + detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent& event) { + m_detach = detach_checkbox->GetValue(); + event.Skip(); // Let CheckBox update its bitmap for the new state. + }); detach_label->SetForegroundColour(wxColour("#363636")); From c57ea0ec67591fa986961dcc59b19185fe190ba6 Mon Sep 17 00:00:00 2001 From: Kiss Lorand <50251547+kisslorand@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:28:26 +0300 Subject: [PATCH 3/6] Fix nozzle type undo and unsaved changes tracking (#15515) --- src/slic3r/GUI/Tab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 644c5258a1..e650c45126 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5130,7 +5130,7 @@ void TabPrinter::build_fff() optgroup->append_single_option_line("adaptive_bed_mesh_margin", "printer_basic_information_adaptive_bed_mesh#mesh-margin"); optgroup = page->new_optgroup(L("Accessory"), "param_accessory"); - optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type"); + optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type", 0); optgroup->append_single_option_line("nozzle_hrc", "printer_basic_information_accessory#nozzle-hrc"); optgroup->append_single_option_line("auxiliary_fan", "printer_basic_information_accessory#auxiliary-part-cooling-fan"); optgroup->append_single_option_line("fan_direction"); From b370d8ef316f77bfe61ba651c3f5b07510fff56b Mon Sep 17 00:00:00 2001 From: Kris Austin Date: Thu, 3 Sep 2026 16:35:10 -0500 Subject: [PATCH 4/6] build: clear 31 warnings - copy and move declarations (#15507) --- src/libslic3r/Measure.hpp | 9 --------- src/libslic3r/Orient.cpp | 1 - src/libslic3r/Point.hpp | 2 -- src/libslic3r/Preset.hpp | 14 +++++++------- src/libslic3r/SLA/Hollowing.hpp | 3 --- src/libslic3r/Support/SupportCommon.cpp | 4 ---- src/libslic3r/Support/TreeSupport.hpp | 5 +++-- src/libslic3r/calib.hpp | 20 +++----------------- src/slic3r/GUI/PartPlate.hpp | 10 ---------- 9 files changed, 13 insertions(+), 55 deletions(-) diff --git a/src/libslic3r/Measure.hpp b/src/libslic3r/Measure.hpp index 2f378f5778..0374c5d4aa 100644 --- a/src/libslic3r/Measure.hpp +++ b/src/libslic3r/Measure.hpp @@ -33,15 +33,6 @@ public: SurfaceFeature(const Vec3d& pt) : m_type{SurfaceFeatureType::Point}, m_pt1{pt} {} - SurfaceFeature(const SurfaceFeature& sf){ - this->clone(sf); - volume = sf.volume; - plane_indices = sf.plane_indices; - world_tran = sf.world_tran; - world_plane_features = sf.world_plane_features; - origin_surface_feature = sf.origin_surface_feature; - } - void clone(const SurfaceFeature &sf) { m_type = sf.get_type(); diff --git a/src/libslic3r/Orient.cpp b/src/libslic3r/Orient.cpp index ae1954087d..7d54048176 100644 --- a/src/libslic3r/Orient.cpp +++ b/src/libslic3r/Orient.cpp @@ -39,7 +39,6 @@ namespace orientation { float height_to_bottom_hull_ratio = 0; // affects stability, the lower the better float unprintability = 0; Eigen::VectorXf areas_cooling; - CostItems(CostItems const & other) = default; CostItems() = default; static std::string field_names() { return " overhang, bottom, bothull, contour, A_laf, A_prj, unprintability"; diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 039f361eaa..21e5355fc5 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -195,7 +195,6 @@ public: Point(int64_t x, int32_t y) : Vec2crd(coord_t(x), coord_t(y)) {} Point(int32_t x, int64_t y) : Vec2crd(coord_t(x), coord_t(y)) {} Point(double x, double y) : Vec2crd(coord_t(std::round(x)), coord_t(std::round(y))) {} - Point(const Point &rhs) { *this = rhs; } explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(std::round(rhs.x())), coord_t(std::round(rhs.y()))) {} // This constructor allows you to construct Point from Eigen expressions // This constructor has to be implicit (non-explicit) to allow implicit conversion from Eigen expressions. @@ -278,7 +277,6 @@ public: Point3(int32_t x, int32_t y, int32_t z = 0) : Vec3crd(coord_t(x), coord_t(y), coord_t(z)) {} Point3(int64_t x, int64_t y, int64_t z = 0) : Vec3crd(coord_t(x), coord_t(y), coord_t(z)) {} Point3(double x, double y, double z = 0.0) : Vec3crd(coord_t(std::round(x)), coord_t(std::round(y)), coord_t(std::round(z))) {} - Point3(const Point3 &rhs) { *this = rhs; } explicit Point3(const Vec2crd& vec2crd, coord_t z = 0) : Vec3crd(vec2crd.x(), vec2crd.y(), z) {} explicit Point3(const Vec3crd &vec3crd) : Vec3crd(vec3crd) {} // This constructor allows you to construct Point from Eigen expressions diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index c9b3197a6f..22089be0b6 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -840,13 +840,12 @@ public: protected: PresetCollection() = default; - // Copy constructor and copy operators are not to be used from outside PresetBundle, - // as the Profile::vendor points to an instance of VendorProfile stored at parent PresetBundle! - PresetCollection(const PresetCollection &other) = default; - //BBS: add operator= logic insteadof default + // Deleted by the std::recursive_mutex member. PresetBundle copies by assignment. + PresetCollection(const PresetCollection &other) = delete; + //BBS: hand-written because m_mutex cannot be copy-assigned. PresetCollection& operator=(const PresetCollection &other); - // After copying a collection with the default operators above, call this function - // to adjust Profile::vendor pointers. + // Copying leaves every Preset::vendor pointing into the source bundle's vendor map. + // This re-points them at the matching entries in vendors. void update_vendor_ptrs_after_copy(const VendorMap &vendors); // Select a preset, if it exists. If it does not exist, select an invalid (-1) index. @@ -984,7 +983,8 @@ public: bool only_default_printers() const; private: PrinterPresetCollection() = default; - PrinterPresetCollection(const PrinterPresetCollection &other) = default; + // Deleted along with the base copy constructor. + PrinterPresetCollection(const PrinterPresetCollection &other) = delete; PrinterPresetCollection& operator=(const PrinterPresetCollection &other) = default; friend class PresetBundle; diff --git a/src/libslic3r/SLA/Hollowing.hpp b/src/libslic3r/SLA/Hollowing.hpp index b57513fe72..f6beaadb24 100644 --- a/src/libslic3r/SLA/Hollowing.hpp +++ b/src/libslic3r/SLA/Hollowing.hpp @@ -44,9 +44,6 @@ struct DrainHole : pos(p), normal(n), radius(r), height(h), failed(fl) {} - DrainHole(const DrainHole& rhs) : - DrainHole(rhs.pos, rhs.normal, rhs.radius, rhs.height, rhs.failed) {} - bool operator==(const DrainHole &sp) const; bool operator!=(const DrainHole &sp) const { return !(sp == (*this)); } diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 0c7a4b832e..b6df219866 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -1234,10 +1234,6 @@ static void modulate_extrusion_by_overlapping_layers( (fragment_end.is_start ? &polyline.points.front() : &polyline.points.back()); } private: - ExtrusionPathFragmentEndPointAccessor& operator=(const ExtrusionPathFragmentEndPointAccessor&) { - return *this; - } - const std::vector &m_path_fragments; }; const coord_t search_radius = 7; diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index e0446ad5f1..61e030ef86 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -204,8 +204,9 @@ public: clear_nodes(); } - TreeSupportData(TreeSupportData&&) = default; - TreeSupportData& operator=(TreeSupportData&&) = default; + // Deleted by the tbb::spin_mutex member. + TreeSupportData(TreeSupportData&&) = delete; + TreeSupportData& operator=(TreeSupportData&&) = delete; TreeSupportData(const TreeSupportData&) = delete; TreeSupportData& operator=(const TreeSupportData&) = delete; diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index ed0f76ee86..abca5e79dc 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -91,29 +91,15 @@ class CaliPresetInfo { public: int tray_id; - int extruder_id; - NozzleVolumeType nozzle_volume_type; - BedType bed_type; + int extruder_id = 0; + NozzleVolumeType nozzle_volume_type{nvtStandard}; + BedType bed_type{btDefault}; float nozzle_diameter; int nozzle_pos_id{-1}; std::string nozzle_sn; std::string filament_id; std::string setting_id; std::string name; - - CaliPresetInfo &operator=(const CaliPresetInfo &other) - { - this->tray_id = other.tray_id; - this->extruder_id = other.extruder_id; - this->nozzle_volume_type = other.nozzle_volume_type; - this->nozzle_diameter = other.nozzle_diameter; - this->nozzle_pos_id = other.nozzle_pos_id; - this->nozzle_sn = other.nozzle_sn; - this->filament_id = other.filament_id; - this->setting_id = other.setting_id; - this->name = other.name; - return *this; - } }; struct PrinterCaliInfo diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 5760320b49..58c0f95b87 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -675,16 +675,6 @@ public: offset = Vec2d(0, 0); } - TexturePart(const TexturePart& part) { - this->x = part.x; - this->y = part.y; - this->w = part.w; - this->h = part.h; - this->offset = part.offset; - this->buffer = part.buffer; - this->filename = part.filename; - this->texture = part.texture; - } void update_pos(float xx, float yy, float ww, float hh) { x = xx; y = yy; From 7acea3ed09b19b58fe0e1fbe2980ad417ddc2764 Mon Sep 17 00:00:00 2001 From: TheLegendTubaGuy <95944177+thelegendtubaguy@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:42:31 -0500 Subject: [PATCH 5/6] Honor symbolic default bed types for new printers (#15273) Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com> --- resources/profiles/Snapmaker.json | 2 +- .../profiles/Snapmaker/machine/fdm_U1.json | 2 +- src/libslic3r/Preset.cpp | 16 +++++--- src/libslic3r/PresetBundle.cpp | 10 +++++ .../libslic3r/test_preset_bundle_loading.cpp | 38 +++++++++++++++++++ 5 files changed, 60 insertions(+), 8 deletions(-) diff --git a/resources/profiles/Snapmaker.json b/resources/profiles/Snapmaker.json index 9a6fab7942..65b1d12a3a 100644 --- a/resources/profiles/Snapmaker.json +++ b/resources/profiles/Snapmaker.json @@ -1,6 +1,6 @@ { "name": "Snapmaker", - "version": "02.04.00.09", + "version": "02.04.00.10", "force_update": "0", "description": "Snapmaker configurations", "machine_model_list": [ diff --git a/resources/profiles/Snapmaker/machine/fdm_U1.json b/resources/profiles/Snapmaker/machine/fdm_U1.json index 717215b507..0b3d89a303 100644 --- a/resources/profiles/Snapmaker/machine/fdm_U1.json +++ b/resources/profiles/Snapmaker/machine/fdm_U1.json @@ -184,7 +184,7 @@ "nozzle_type": "undefine", "auxiliary_fan": "0", "support_multi_bed_types": "1", - "default_bed_type": "4", + "default_bed_type": "Textured PEI Plate", "printable_area": [ "0.5x1", "270.5x1", diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 304c8957d5..235ea66ad2 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -983,15 +983,19 @@ BedType Preset::get_default_bed_type(PresetBundle* preset_bundle) if (config.has("default_bed_type") && !config.opt_string("default_bed_type").empty()) { try { std::string str_bed_type = config.opt_string("default_bed_type"); - - // Try parsing as integer first (legacy format) + BedType bed_type; + if (ConfigOptionEnum::from_string(str_bed_type, bed_type) && + bed_type > btDefault && bed_type < btCount) { + return bed_type; + } + + // Try parsing as integer (legacy format) int bed_type_value = atoi(str_bed_type.c_str()); - if (bed_type_value > 0) { + if (bed_type_value > 0 && bed_type_value < BedType::btCount) { return BedType(bed_type_value); } - else { - BOOST_LOG_TRIVIAL(error) << "default_bed_type: invalid bed type: " << str_bed_type; - } + + BOOST_LOG_TRIVIAL(error) << "default_bed_type: invalid bed type: " << str_bed_type; return BedType::btPEI; } catch(...) { diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 8e5d565c55..2dfc967e3f 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2920,6 +2920,16 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p // If executed due to a Config Wizard update, preferred_printer contains the first newly installed printer, otherwise nullptr. const Preset *preferred_printer = printers.find_system_preset_by_model_and_variant(preferred_selection.printer_model_id, preferred_selection.printer_variant); printers.select_preset_by_name(preferred_printer ? preferred_printer->name : initial_printer_profile_name, true); + Preset &selected_printer = printers.get_edited_preset(); + if (selected_printer.printer_technology() == ptFFF) { + BedType bed_type = selected_printer.get_default_bed_type(this); + const std::string saved_bed_type = config.get_printer_setting(selected_printer.name, "curr_bed_type"); + const int saved_bed_type_value = atoi(saved_bed_type.c_str()); + if (saved_bed_type_value > btDefault && saved_bed_type_value < btCount) + bed_type = static_cast(saved_bed_type_value); + project_config.set_key_value("curr_bed_type", new ConfigOptionEnum(bed_type)); + config.set("curr_bed_type", std::to_string(static_cast(bed_type))); + } CNumericLocalesSetter locales_setter; // Orca: load from orca_presets diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 55c18bfa9e..c75a05949e 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -184,6 +184,44 @@ TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][ CHECK(bundle.get_printer_extruder_count() == 2); } +TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundle]") +{ + PresetBundle bundle; + Preset& printer = add_inmemory_preset(bundle.printers, "Test Printer"); + printer.is_system = true; + printer.config.option("printer_model")->value = "TEST-MODEL"; + printer.config.option("printer_variant")->value = "0.4"; + printer.config.option("default_bed_type")->value = "Engineering Plate"; + + AppConfig app_config; + app_config.set("curr_bed_type", std::to_string(static_cast(btPTE))); + PresetBundle::PresetPreferences preferred_selection; + BedType expected_bed_type; + + SECTION("New printer uses its symbolic default") { + expected_bed_type = btEP; + preferred_selection = {"TEST-MODEL", "0.4"}; + } + SECTION("Re-enabled printer uses its saved selection") { + expected_bed_type = btPC; + preferred_selection = {"TEST-MODEL", "0.4"}; + app_config.set_printer_setting("Test Printer", "curr_bed_type", + std::to_string(static_cast(expected_bed_type))); + } + SECTION("Existing printer keeps its saved selection after presets reload") { + expected_bed_type = btPCT; + app_config.set("presets", PRESET_PRINTER_NAME, "Test Printer"); + app_config.set_printer_setting("Test Printer", "curr_bed_type", + std::to_string(static_cast(expected_bed_type))); + } + + bundle.load_selections(app_config, preferred_selection); + bundle.export_selections(app_config); + + CHECK(bundle.project_config.opt_enum("curr_bed_type") == expected_bed_type); + CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast(expected_bed_type))); +} + TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]") { RenameTestCollection coll; From 57ce18d70d0e6084884d2cb8bcf07792a852e74b Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:31:42 +0800 Subject: [PATCH 6/6] [CLI]: CLI Argument Parsing Fixes (#15478) * Reject invalid CLI argument values instead of silently accepting them * Add read_cli accept/reject tests * Update Option Type for LogFile argument * Add read_cli vector option tests * Accept common bool spellings on the CLI, cover --logfile in tests * Add unit tests for truthy bool parsing --- src/OrcaSlicer.cpp | 2 +- src/libslic3r/Config.cpp | 53 +++++++- src/libslic3r/PrintConfig.cpp | 8 +- tests/libslic3r/test_config.cpp | 223 ++++++++++++++++++++++++++++++++ 4 files changed, 274 insertions(+), 12 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 33669abcf2..ab26a3f7c8 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -7384,7 +7384,7 @@ void CLI::print_help(bool include_print_options, PrinterTechnology printer_techn << std::endl << "Print setting priorities:" << std::endl << "\t1) setting values from the command line (highest priority)"<< std::endl - << "\t2) setting values loaded with --load_settings and --load_filaments" << std::endl + << "\t2) setting values loaded with --load-settings and --load-filaments" << std::endl << "\t3) setting values loaded from 3mf(lowest priority)" << std::endl; /*if (include_print_options) { diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 242e4bb146..a2756a9f8e 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -1715,6 +1715,36 @@ const ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key) co return (it == options.end()) ? nullptr : it->second.get(); } +// ConfigOptionBool(s)::deserialize only understands "1" and "0", but scripts commonly spell CLI +// flags as --opt=true or --opt=no. Map the usual spellings onto what deserialize() accepts, per +// comma-separated item so vector options keep working, and pass anything else through unchanged +// so a genuine typo is still reported as invalid. +static std::string normalize_cli_bool_value(const std::string &value) +{ + static const char* true_values[] = { "1", "true", "yes", "on", "enabled" }; + static const char* false_values[] = { "0", "false", "no", "off", "disabled" }; + + auto matches = [](const std::string &item, const char* const* candidates, size_t count) { + return std::any_of(candidates, candidates + count, [&item](const char* candidate) { return boost::iequals(item, candidate); }); + }; + + std::string normalized; + std::istringstream is(value); + std::string item; + while (std::getline(is, item, ',')) { + boost::trim(item); + if (! normalized.empty()) + normalized += ","; + if (matches(item, true_values, std::size(true_values))) + normalized += "1"; + else if (matches(item, false_values, std::size(false_values))) + normalized += "0"; + else + normalized += item; + } + return normalized; +} + bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option_keys* extra, t_config_option_keys* keys) { // cache the CLI option => opt_key mapping @@ -1812,17 +1842,32 @@ bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option // to the end of the value. if (opt_base->type() == coBools && value.empty()) static_cast(opt_base)->values.push_back(!no); - else + else { // Deserialize any other vector value (ConfigOptionInts, Floats, Percents, Points) the same way // they get deserialized from an .ini file. For ConfigOptionStrings, that means that the C-style unescape // will be applied for values enclosed in quotes, while values non-enclosed in quotes are left to be // unescaped by the calling shell. - opt_vector->deserialize(value, true); + const std::string vector_value = opt_base->type() == coBools ? normalize_cli_bool_value(value) : value; + bool deserialized = false; + try { + deserialized = opt_vector->deserialize(vector_value, true); + } catch (const std::exception &ex) { + // e.g. "nil" deserialized into a non-nullable vector option throws instead of + // returning false - treat that the same as any other invalid value here. + deserialized = false; + } + if (! deserialized) { + boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl; + return false; + } + } } else if (opt_base->type() == coBool) { if (value.empty()) static_cast(opt_base)->value = !no; - else - opt_base->deserialize(value); + else if (! opt_base->deserialize(normalize_cli_bool_value(value))) { + boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl; + return false; + } } else if (opt_base->type() == coString) { // Do not unescape single string values, the unescaping is left to the calling shell. static_cast(opt_base)->value = value; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 29362fcf39..7d93341b98 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -11990,13 +11990,11 @@ CLIActionsConfigDef::CLIActionsConfigDef() def = this->add("load_defaultfila", coBool); def->label = L("Load default filaments"); def->tooltip = L("Load first filament as default for those not loaded."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("min_save", coBool); def->label = L("Minimum save"); def->tooltip = L("Export 3MF with minimum size."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("mtcpp", coInt); @@ -12022,7 +12020,6 @@ CLIActionsConfigDef::CLIActionsConfigDef() def = this->add("normative_check", coBool); def->label = L("Normative check"); def->tooltip = L("Check the normative items."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(true)); /*def = this->add("help_fff", coBool); @@ -12289,7 +12286,7 @@ CLIMiscConfigDef::CLIMiscConfigDef() def->cli_params = "level"; def->set_default_value(new ConfigOptionInt(1)); - def = this->add("logfile", coInt); + def = this->add("logfile", coString); def->label = L("Log file"); def->tooltip = L("Redirects debug logging to file.\n"); def->cli_params = "file"; @@ -12337,7 +12334,6 @@ CLIMiscConfigDef::CLIMiscConfigDef() def = this->add("skip_modified_gcodes", coBool); def->label = L("Skip modified G-code in 3MF"); def->tooltip = L("Skip the modified G-code in 3MF from printer or filament presets."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("makerlab_name", coString); @@ -12367,14 +12363,12 @@ CLIMiscConfigDef::CLIMiscConfigDef() def = this->add("allow_newer_file", coBool); def->label = L("Allow 3MF with newer version to be sliced"); def->tooltip = L("Allow 3MF with newer version to be sliced."); - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); def = this->add("allow_mix_temp", coBool); // internal use only, don't need translation def->label = "Allow filaments with high/low temperature to be printed together"; def->tooltip = "Allow filaments with high/low temperature to be printed together."; - def->cli_params = "option"; def->set_default_value(new ConfigOptionBool(false)); } diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 12b161322d..2627c3cda0 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -828,3 +828,226 @@ SCENARIO("ConfigOptionVector::set_to_index throws on incompatible type", "[Confi } } } + +TEST_CASE("read_cli applies valid values and collects non-option arguments", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--nozzle-temperature", "210,190", "--reduce-crossing-wall=1", "model.3mf"}; + REQUIRE(config.read_cli(5, argv, &extra, &keys)); + REQUIRE(config.opt("nozzle_temperature")->values == std::vector{210, 190}); + REQUIRE(config.opt("reduce_crossing_wall")->value); + REQUIRE(extra == t_config_option_keys{"model.3mf"}); + REQUIRE(keys == t_config_option_keys{"nozzle_temperature", "reduce_crossing_wall"}); +} + +TEST_CASE("read_cli rejects nil for a non-nullable vector option", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--nozzle-temperature", "nil"}; + REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys)); +} + +TEST_CASE("read_cli rejects an invalid boolean value", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--reduce-crossing-wall=maybe"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +TEST_CASE("read_cli accepts the common spellings of a boolean value", "[Config]") { + const auto [text, expected] = GENERATE(table({ + {"--reduce-crossing-wall=1", true }, + {"--reduce-crossing-wall=true", true }, + {"--reduce-crossing-wall=Yes", true }, + {"--reduce-crossing-wall=on", true }, + {"--reduce-crossing-wall=enabled", true }, + {"--reduce-crossing-wall=TRUE", true }, + {"--reduce-crossing-wall=oN", true }, + {"--reduce-crossing-wall=0", false}, + {"--reduce-crossing-wall=false", false}, + {"--reduce-crossing-wall=No", false}, + {"--reduce-crossing-wall=off", false}, + {"--reduce-crossing-wall=disabled", false}, + {"--reduce-crossing-wall=FALSE", false}, + {"--reduce-crossing-wall=DiSaBlEd", false}, + })); + + DYNAMIC_SECTION(text) { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", text}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt("reduce_crossing_wall")->value == expected); + } +} + +TEST_CASE("read_cli accepts the common boolean spellings inside a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true,no,1"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt("filament_soluble")->values == std::vector{1, 0, 1}); +} + +TEST_CASE("read_cli trims whitespace around boolean spellings", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--reduce-crossing-wall= true ", "--filament-soluble= true , no ,1"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt("reduce_crossing_wall")->value); + REQUIRE(config.opt("filament_soluble")->values == std::vector{1, 0, 1}); +} + +TEST_CASE("read_cli normalizes boolean spellings when a bools vector is repeated", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true", "--filament-soluble=off"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt("filament_soluble")->values == std::vector{1, 0}); +} + +TEST_CASE("read_cli keeps nil alongside boolean spellings in a nullable bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--enable-overhang-speed=nil,yes,off"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + auto* opt = config.opt("enable_overhang_speed"); + REQUIRE(opt != nullptr); + REQUIRE(opt->values.size() == 3); + REQUIRE(opt->is_nil(0)); + REQUIRE(opt->values[1] == 1); + REQUIRE(opt->values[2] == 0); +} + +TEST_CASE("read_cli rejects an empty item inside a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true,,1"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +TEST_CASE("read_cli rejects an unknown spelling next to a valid one in a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=true,affirmative"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +// The normalization lives in read_cli's boolean branches, so options of other types keep the +// value verbatim - a path named "on" or a colour named "true" must not turn into "1". +TEST_CASE("read_cli leaves boolean spellings alone for non-boolean options", "[Config]") { + SECTION("string option") { + Slic3r::DynamicPrintAndCLIConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--logfile=true"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt("logfile")->value == "true"); + } + SECTION("strings vector option") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-colour=on;off"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt("filament_colour")->values == std::vector{"on", "off"}); + } +} + +TEST_CASE("read_cli treats a bare boolean flag as true without consuming the next argument", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--reduce-crossing-wall", "model.3mf"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt("reduce_crossing_wall")->value); + REQUIRE(extra == t_config_option_keys{"model.3mf"}); +} + +TEST_CASE("read_cli rejects an invalid scalar numeric value", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--top-shell-layers", "several"}; + REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys)); +} + +TEST_CASE("read_cli appends values when a vector option is repeated", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--nozzle-temperature", "210", "--nozzle-temperature", "190,200"}; + REQUIRE(config.read_cli(5, argv, &extra, &keys)); + REQUIRE(config.opt("nozzle_temperature")->values == std::vector{210, 190, 200}); + // the key is recorded once, on first use + REQUIRE(keys == t_config_option_keys{"nozzle_temperature"}); +} + +TEST_CASE("read_cli parses a bools vector given in the --flag=values form", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=1,0,1"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt("filament_soluble")->values == std::vector{1, 0, 1}); +} + +TEST_CASE("read_cli rejects an invalid value inside a bools vector", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble=1,maybe"}; + REQUIRE_FALSE(config.read_cli(2, argv, &extra, &keys)); +} + +TEST_CASE("read_cli appends true for a bare bools vector flag", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-soluble"}; + REQUIRE(config.read_cli(2, argv, &extra, &keys)); + REQUIRE(config.opt("filament_soluble")->values == std::vector{1}); +} + +TEST_CASE("read_cli splits a strings vector on semicolons and unescapes quoted items", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-colour", "#FF0000;\"a\\nb\";#00FF00"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + auto& values = config.opt("filament_colour")->values; + REQUIRE(values == std::vector{"#FF0000", "a\nb", "#00FF00"}); +} + +TEST_CASE("read_cli rejects a strings vector with an unterminated quote", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-colour", "\"oops"}; + REQUIRE_FALSE(config.read_cli(3, argv, &extra, &keys)); +} + +TEST_CASE("read_cli parses a points vector in the NxM coordinate form", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--printable-area", "0x0,200x0,200x200,0x200"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + auto& points = config.opt("printable_area")->values; + REQUIRE(points.size() == 4); + REQUIRE_THAT(points[1].x(), Catch::Matchers::WithinAbs(200.0, 1e-9)); + REQUIRE_THAT(points[1].y(), Catch::Matchers::WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(points[3].x(), Catch::Matchers::WithinAbs(0.0, 1e-9)); + REQUIRE_THAT(points[3].y(), Catch::Matchers::WithinAbs(200.0, 1e-9)); +} + +// logfile is a CLI-only option, so it needs the config type whose def pulls in cli_misc_config_def. +TEST_CASE("read_cli stores the log file path as a string", "[Config]") { + Slic3r::DynamicPrintAndCLIConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--logfile", "orca.log"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + REQUIRE(config.opt("logfile")->value == "orca.log"); +} + +TEST_CASE("read_cli accepts nil entries for a nullable vector option", "[Config]") { + Slic3r::DynamicPrintConfig config; + t_config_option_keys extra, keys; + const char* argv[] = {"orca-slicer", "--filament-retraction-length", "nil,2.5"}; + REQUIRE(config.read_cli(3, argv, &extra, &keys)); + auto* opt = config.opt("filament_retraction_length"); + REQUIRE(opt != nullptr); + REQUIRE(opt->values.size() == 2); + REQUIRE(opt->is_nil(0)); + REQUIRE_FALSE(opt->is_nil(1)); + REQUIRE_THAT(opt->values[1], Catch::Matchers::WithinAbs(2.5, 1e-9)); +}