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/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/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 { 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/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.cpp b/src/libslic3r/Preset.cpp index 460276c420..d441d76690 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/Preset.hpp b/src/libslic3r/Preset.hpp index 85908b95a5..b38bc77930 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -856,13 +856,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. @@ -1000,7 +999,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/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 4c4114345e..da5696840b 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -2941,6 +2941,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/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/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; 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")); diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 962d59d9ca..2dedb23365 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5131,7 +5131,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"); 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)); +} diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index d2d0560be2..9526a64b02 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -220,6 +220,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;