Compare commits

...

38 Commits

Author SHA1 Message Date
ExPikaPaka
d7b49d9d68 Transfer all Preset fields from cache via move assignmet
apply_vendor_preset_group was copying fields manually and missed
bundle_id, user_id, base_id, sync_info, updated_time, key_values,
ini_str. Replace field-by-field copy with move assignment of the
fully-deserialized Preset, then restore the vendor pointer which
is excluded from serialization.
2026-07-07 12:10:11 +02:00
ExPikaPaka
f512872e35 Rvert from per-verndor to single cache file
Replace N per-vendor .cache files with a single system_presets.cache
that holds all vendors and presets in one serialized blob.

Cache load is now all-or-nothing: on hit all vendors are applied from
the bundle (sub-second); on miss all vendors are parsed from JSON and
a fresh bundle is written to the user cache dir.

Invalidation is driven by bundle_key - a sorted concatenation of all
vendor JSON version strings. Any vendor update invalidates the whole
cache and triggers re-parse on next launch.

Guide wizard (WebGuideDialog) loads the bundled cache into a plain
PresetBundle instead of a separate VendorGuideData struct, removing
the duplicate data model.

generate_system_cache simplified from a per-vendor loop to a single
save_system_presets_cache() call producing one output file.
2026-07-07 08:48:32 +02:00
ExPikaPaka
cb093b8fba Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-07-06 12:24:54 +02:00
ExPikaPaka
34fae25ed4 2026-07-06 12:24:47 +02:00
ExPikaPaka
c014f0403b Add a bit more tests 2026-07-06 10:20:13 +02:00
ExPikaPaka
3765549786 Add tests for Cache system 2026-07-06 09:53:58 +02:00
ExPikaPaka
d7bacd5c1c Minimize field duplication by moving Cache thing into PresetBundle 2026-07-06 08:37:32 +02:00
ExPikaPaka
fd3bee3c62 Serealize all value fields for Preset class to minimize regression later 2026-07-06 08:16:23 +02:00
Gabriel Monteiro
fd80ded5a8 fix(gui): startup crash in clang/LLVM builds, null pointer UB in create_scaled_bitmap (#14521)
fix(gui): avoid null-pointer UB in create_scaled_bitmap with win == nullptr

create_scaled_bitmap() documents that win may be nullptr, but called
win->FromDIP() on it. Calling a member function through a null pointer
is undefined behavior: clang assumes `this` is non-null and deletes the
subsequent `win ?` null check added in #13117, turning the fallback
branch into an unconditional virtual call through a null vtable.
This crashed LLVM/clang-cl builds at startup (access violation reading
0x0 in BBLTopbar creation); MSVC builds were unaffected by luck.

Use the static, null-safe wxWindow::FromDIP(x, win) overload instead,
which falls back to the primary display DPI. Behavior is unchanged for
non-null windows.
2026-07-06 11:28:03 +08:00
Kiss Lorand
db2f861a11 Fix painted mouse-ear brims snapping to wrong location (#14606)
Fix painted mouse-ear brims moving to incorrect locations when Brim follows compensated outline is enabled.
2026-07-06 09:28:41 +08:00
raistlin7447
2860353b9f fix: multi-user slicing crash on shared temp dir (#14607)
On Linux every account shares /tmp, but slicing builds temp paths there under
fixed, app-owned names via temporary_dir() (model backups, STEP import,
part-skip). The first user to slice creates and owns those dirs, so the next
user cannot write under them and slicing crashes with "No such file or
directory".

Tag the app temp root with the user id at startup (<temp>/orcaslicer_<uid>)
so every temporary_dir() consumer is isolated at once. The id stays at the
top level of the world-writable system temp so each user's dir is created
directly there; a shared parent dir would be owned by whichever user made it
first. The root is pre-created because STEP import writes into it directly.
Windows keeps the plain temp dir since it is already per-user.

Fixes #10108. Same root cause as #5969.
2026-07-06 09:18:04 +08:00
Alexandre Folle de Menezes
218ec29d74 Improve and complement ptBR translation (#14470)
Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-07-05 21:29:46 +03:00
ExPikaPaka
e5ed64a307 Update check for stale cache 2026-07-03 07:46:51 +02:00
ExPikaPaka
47fd55d462 Revert json cache back 2026-07-03 07:46:06 +02:00
SoftFever
b90439adba Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-07-01 16:21:11 +08:00
ExPikaPaka
31270d4027 Fix build for windows arm64 2026-07-01 08:50:58 +02:00
Rodrigo Faselli
c9afb98ca2 Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-06-28 21:27:14 -03:00
Rodrigo Faselli
d102157138 Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-06-26 07:47:50 -03:00
ExPikaPaka
f088a18167 Remove leftover cache file 2026-06-18 10:38:05 +02:00
ExPikaPaka
f2f5bea4bf Skip invalid vendors 2026-06-18 10:37:04 +02:00
ExPikaPaka
0cd9e77e95 Remove BOM added by VSC 2026-06-18 08:52:42 +02:00
ExPikaPaka
f35c2b1ef7 Use get_vendor_cache_key() to match cache keys written by the app 2026-06-18 08:35:13 +02:00
ExPikaPaka
97dee9349b Fix use-after-free in CallAfter lambda; replace raw thread pointer with unique_ptr 2026-06-18 08:35:03 +02:00
ExPikaPaka
493597f132 Remove CachedPrinterModel/VendorProfile/Preset mirror structs from VendorCache 2026-06-18 08:34:50 +02:00
ExPikaPaka
517fa29d6f Add cereal serialize() to VendorProfile, PrinterModel, Preset, and Semver 2026-06-18 08:34:27 +02:00
ExPikaPaka
2ab9e14525 Simplify code a bit more 2026-06-17 09:17:07 +02:00
ExPikaPaka
b6a1546ff5 Merge branch 'feature/cache_profiles_and_optimize_loading_speed' of https://github.com/OrcaSlicer/OrcaSlicer into feature/cache_profiles_and_optimize_loading_speed 2026-06-17 08:50:38 +02:00
ExPikaPaka
6e36411736 Simplify code by mergin it in PresetBundle 2026-06-17 08:46:06 +02:00
ExPikaPaka
0ec6c03c83 Generate cache per vendor 2026-06-17 08:45:24 +02:00
SoftFever
7a8f5a88c9 Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed 2026-06-16 16:27:46 +08:00
ExPikaPaka
83e1712ded Add inspecting tool and fix CI cache generation 2026-06-16 08:57:33 +02:00
ExPikaPaka
80fbf3b405 Add cache to GuideDialog as previos version didn't work as expected 2026-06-16 08:57:09 +02:00
ExPikaPaka
5b80d0cc07 Handle corrupted files 2026-06-16 08:56:25 +02:00
ExPikaPaka
88901a969f Add partial cache generation when only one of the vendros is changed to speed up recalculation time 2026-06-15 09:28:59 +02:00
ExPikaPaka
5d0c640f7b Add CI\CD step to prepare cache file in ahead of time so user does not need to wait 2026-06-15 09:19:53 +02:00
ExPikaPaka
d861e8af22 Integrate caching into WebGuideDialog which speeds up time of SetupWizzard and PrinterSelection dialog 2026-06-15 08:52:55 +02:00
ExPikaPaka
6186436b23 Removing user\bundle serialization and keeping it only for system presets 2026-06-15 08:31:33 +02:00
ExPikaPaka
604f15e20d Add caching system for presets 2026-06-11 08:52:44 +02:00
90 changed files with 2048 additions and 549 deletions

View File

@@ -144,6 +144,15 @@ jobs:
run: |
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15
- name: Generate system presets cache (macOS)
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
shell: bash
run: |
tool=$(find build/${{ inputs.arch }} -name generate_system_cache -type f | head -1)
profiles=$(find build/${{ inputs.arch }} -path "*/Resources/profiles" -type d | head -1)
"$tool" --path "$profiles" --log_level 2
- name: Pack macOS app bundle ${{ inputs.arch }}
if: runner.os == 'macOS' && !inputs.macos-combine-only
working-directory: ${{ github.workspace }}
@@ -340,6 +349,23 @@ jobs:
if ($arch -eq "arm64") { .\build_release_vs.bat slicer arm64 } else { .\build_release_vs.bat slicer }
shell: pwsh
- name: Generate system presets cache (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$buildDir = $env:BUILD_DIR
$tool = Get-ChildItem -Recurse -Path $buildDir -Filter "generate_system_cache.exe" | Select-Object -First 1
if (-not $tool) { Write-Error "generate_system_cache.exe not found in $buildDir"; exit 1 }
$profiles = Get-ChildItem -Recurse -Path $buildDir -Directory -Filter profiles |
Where-Object { $_.FullName -match 'resources' } | Select-Object -First 1
if (-not $profiles) { Write-Error "profiles directory not found in $buildDir"; exit 1 }
# Add the slicer's runtime DLL directory to PATH so generate_system_cache.exe
# can resolve its dependencies (TKernel.dll etc.) without a full install step.
$dll_dir = Get-ChildItem -Recurse -Path $buildDir -Filter "TKernel.dll" |
Select-Object -First 1 | Select-Object -ExpandProperty DirectoryName
if ($dll_dir) { $env:PATH = "$dll_dir;$env:PATH" }
& $tool.FullName --path $profiles.FullName --log_level 2
# NSIS is x86-only; it runs (and the installer it emits runs) under ARM64's
# x86 emulation, packaging the native arm64 payload from build-arm64.
- name: Create installer Win
@@ -477,6 +503,22 @@ jobs:
retention-days: 5
if-no-files-found: error
- name: Generate system presets cache (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
tool=$(find build -name generate_system_cache -type f | head -1)
if [ -z "$tool" ]; then echo "ERROR: generate_system_cache not found in build tree" >&2; exit 1; fi
"$tool" --path build/package/resources/profiles --log_level 2
# Re-pack the AppImage so the per-vendor caches are included
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
chmod +x "$appimage"
"$appimage" --appimage-extract
cp build/package/resources/profiles/*.cache squashfs-root/resources/profiles/
appimagetool=$(find build -name "appimagetool.AppImage" | head -1)
ARCH=$(uname -m) "$appimagetool" --appimage-extract-and-run squashfs-root "$appimage"
rm -rf squashfs-root
- name: Run external slicer regression tests
if: runner.os == 'Linux' && inputs.arch != 'aarch64'
timeout-minutes: 20

View File

@@ -567,6 +567,8 @@ if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer
echo "Building OrcaSlicer_profile_validator .."
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target OrcaSlicer_profile_validator
echo "Building generate_system_cache ..."
print_and_run cmake --build $BUILD_DIR --config "${BUILD_CONFIG}" --target generate_system_cache
./scripts/run_gettext.sh
fi
if [[ -n "${BUILD_TESTS}" ]] ; then

45
gen_cache.bat Normal file
View File

@@ -0,0 +1,45 @@
@echo off
setlocal
:: Usage: gen_cache.bat [Release|Debug|RelWithDebInfo]
:: Generates per-vendor .cache files into resources/profiles/
:: using generate_system_cache.exe from the local build tree.
set BUILD_CONFIG=%~1
if "%BUILD_CONFIG%"=="" set BUILD_CONFIG=Release
set REPO=%~dp0
set BUILD_DIR=%REPO%build
set EXE=%BUILD_DIR%\src\dev-utils\%BUILD_CONFIG%\generate_system_cache.exe
set PROFILES=%REPO%resources\profiles
if not exist "%EXE%" (
echo ERROR: %EXE% not found.
echo Build the project first with ORCA_TOOLS=ON, config %BUILD_CONFIG%.
exit /b 1
)
if not exist "%PROFILES%" (
echo ERROR: profiles dir not found: %PROFILES%
exit /b 1
)
:: DLLs live in the main Release output, not next to the tool exe
set PATH=%BUILD_DIR%\src\%BUILD_CONFIG%;%PATH%
echo Generating system presets cache...
echo Tool : %EXE%
echo Profiles: %PROFILES%
echo.
"%EXE%" --path "%PROFILES%" --log_level 2
if %ERRORLEVEL% neq 0 (
echo.
echo ERROR: generate_system_cache failed ^(exit %ERRORLEVEL%^).
exit /b %ERRORLEVEL%
)
echo.
echo Done. Cache files written to: %PROFILES%
dir /b "%PROFILES%\*.cache" 2>nul
endlocal

File diff suppressed because it is too large Load Diff

51
remove_cache.bat Normal file
View File

@@ -0,0 +1,51 @@
@echo off
setlocal
:: Removes all OrcaSlicer cache files so the next launch rebuilds from scratch.
::
:: Clears:
:: resources/profiles/*.cache bundled per-vendor binary caches
:: %APPDATA%\OrcaSlicer\system\*.cache user per-vendor binary caches
:: %APPDATA%\OrcaSlicer\guide_profile_cache.json guide JSON cache
set REPO=%~dp0
set PROFILES=%REPO%resources\profiles
set USER_SYSTEM=%APPDATA%\OrcaSlicer\system
set GUIDE_CACHE=%APPDATA%\OrcaSlicer\guide_profile_cache.json
echo Removing OrcaSlicer cache files...
echo.
:: Bundled caches
set FOUND=0
for %%F in ("%PROFILES%\*.cache") do (
del "%%F"
echo Deleted: %%~nxF ^(bundled^)
set FOUND=1
)
if "%FOUND%"=="0" echo No bundled .cache files found in resources\profiles\
echo.
:: User per-vendor caches
set FOUND=0
for %%F in ("%USER_SYSTEM%\*.cache") do (
del "%%F"
echo Deleted: %%~nxF ^(user^)
set FOUND=1
)
if "%FOUND%"=="0" echo No user .cache files found in %USER_SYSTEM%\
echo.
:: Guide JSON cache
if exist "%GUIDE_CACHE%" (
del "%GUIDE_CACHE%"
echo Deleted: guide_profile_cache.json
) else (
echo No guide_profile_cache.json found
)
echo.
echo Done. Next launch will rebuild all caches from JSON.
endlocal

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resources/profiles/LH.cache Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
resources/profiles/iQ.cache Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -111,6 +111,8 @@ if(ORCA_TOOLS)
endif()
target_link_libraries(OrcaSlicer_profile_validator libslic3r boost_headeronly libcurl OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(OrcaSlicer_profile_validator PRIVATE -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
endif()
# Create a slic3r executable

View File

@@ -1317,7 +1317,13 @@ int CLI::run(int argc, char **argv)
return CLI_INVALID_PARAMS;
}
BOOST_LOG_TRIVIAL(info) << "finished setup params, argc="<< argc << std::endl;
std::string temp_path = wxFileName::GetTempDir().utf8_str().data();
std::string temp_path = per_user_temp_dir(wxFileName::GetTempDir().utf8_str().data(), per_user_temp_id());
// Some consumers write into the temp root directly, so create it up front.
try {
boost::filesystem::create_directories(temp_path);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(warning) << "failed to create per-user temp dir " << temp_path << ": " << ex.what();
}
set_temporary_dir(temp_path);
m_extra_config.apply(m_config, true);

View File

@@ -20,6 +20,16 @@ if (SLIC3R_ENC_CHECK)
)
endif()
if (ORCA_TOOLS)
set(_DEV_DEFS -DBOOST_ALL_NO_LIB -DBOOST_USE_WINAPI_VERSION=0x602 -DBOOST_SYSTEM_USE_UTF8)
# generate_system_cache: pre-generates per-vendor resources/profiles/<id>.cache files for CI bundling.
add_executable(generate_system_cache generate_system_cache.cpp)
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
endif()
# Function that adds source file encoding check to a target
# using the above encoding-check binary

View File

@@ -0,0 +1,88 @@
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
#include <iostream>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace po = boost::program_options;
int main(int argc, char* argv[])
{
po::options_description desc("OrcaSlicer System Cache Generator\nUsage");
// clang-format off
desc.add_options()
("help,h", "Show help")
#ifdef __APPLE__
("path,p", po::value<std::string>()->default_value("../../../../../../../resources/profiles"), "Path to profiles directory")
#else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
// clang-format on
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
if (vm.count("help")) { std::cout << desc << "\n"; return 0; }
po::notify(vm);
} catch (const po::error& e) {
std::cerr << "Error: " << e.what() << "\n" << desc << "\n";
return 1;
}
const std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>();
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
return 1;
}
set_logging_level(log_level);
set_data_dir(profiles_path);
set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string());
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
if (!fs::exists(user_dir))
fs::create_directories(user_dir);
AppConfig app_config;
app_config.set("preset_folder", "default");
auto preset_bundle = std::make_unique<PresetBundle>();
preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true);
std::cout << "Loading system presets from: " << profiles_path << "\n";
try {
preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
} catch (const std::exception& ex) {
std::cerr << "Failed to load presets: " << ex.what() << "\n";
return 1;
}
const std::string output_path =
(fs::path(profiles_path) / "system_presets.cache").make_preferred().string();
std::cout << "Saving single-bundle cache to: " << output_path << "\n";
const auto stats = preset_bundle->save_system_presets_cache(profiles_path, output_path);
if (!stats.ok) {
std::cerr << "ERROR: verification failed\n";
return 1;
}
std::cout << "[ok] system_presets.cache\n"
<< " Total print presets: " << stats.print_presets << "\n"
<< " Total filament presets: " << stats.filament_presets << "\n"
<< " Total printer presets: " << stats.printer_presets << "\n";
return 0;
}

View File

@@ -61,7 +61,7 @@ static bool use_brim_efc_outline(const PrintObject &object)
&& object.config().raft_layers.value == 0;
}
//ORCA: Helper for snapping painted ears to the EFC outline.
//ORCA: Helper for projecting painted ears to the EFC outline.
static bool closest_point_on_expolygons(const ExPolygons &polygons, const Point &from, Point &closest_out)
{
double min_dist2 = std::numeric_limits<double>::max();
@@ -69,23 +69,22 @@ static bool closest_point_on_expolygons(const ExPolygons &polygons, const Point
for (const ExPolygon &poly : polygons) {
for (int i = 0; i < poly.num_contours(); ++i) {
const Point *candidate = poly.contour_or_hole(i).closest_point(from);
if (candidate == nullptr)
continue;
const int64_t dx = int64_t(candidate->x()) - int64_t(from.x());
const int64_t dy = int64_t(candidate->y()) - int64_t(from.y());
const double dist2 = double(dx * dx + dy * dy);
if (dist2 < min_dist2) {
min_dist2 = dist2;
closest_out = *candidate;
found = true;
const Lines lines = poly.contour_or_hole(i).lines();
for (const Line &line : lines) {
Point candidate;
const double dist2 = line.distance_to_squared(from, &candidate);
if (dist2 < min_dist2) {
min_dist2 = dist2;
closest_out = candidate;
found = true;
}
}
}
}
return found;
}
//ORCA: Helper for matching painted ears to their original island before EFC snapping.
//ORCA: Helper for matching painted ears to their original island before EFC projection.
static int find_containing_expolygon_index(const ExPolygons &polygons, const Point &from)
{
for (size_t idx = 0; idx < polygons.size(); ++idx) {
@@ -95,7 +94,7 @@ static int find_containing_expolygon_index(const ExPolygons &polygons, const Poi
return -1;
}
//ORCA: Keep painted ear snapping on the matching island when using EFC outline.
//ORCA: Keep painted ear projection on the matching island when using EFC outline.
static bool closest_point_on_matching_island(const ExPolygons &raw_outline, const ExPolygons &efc_outline, const Point &from, Point &closest_out)
{
const int island_idx = find_containing_expolygon_index(raw_outline, from);
@@ -106,6 +105,7 @@ static bool closest_point_on_matching_island(const ExPolygons &raw_outline, cons
}
return closest_point_on_expolygons(efc_outline, from, closest_out);
}
//ORCA: Use post-processed first-layer slices (including EFC) for brim outline.
// Returns ExPolygons of the bottom layer after all first-layer modifiers
// (including elephant foot compensation, if enabled) have been applied.
@@ -358,11 +358,12 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
if (brim_ear_points.size() <= 0) {
return mouse_ears_ex;
}
//ORCA: Painted ears can snap to the EFC-adjusted outline when enabled.
//ORCA: Painted ears follow the EFC-adjusted outline when enabled, while
// preserving their position along the selected outline segment.
const bool use_efc_outline = use_brim_efc_outline(*object);
const ExPolygons &raw_outline = object->layers().front()->lslices;
//ORCA: Lazily computed EFC-adjusted bottom outline.
//Stored separately so we can avoid recomputation unless EFC snapping is used.
//Stored separately so we can avoid recomputation unless EFC projection is used.
ExPolygons efc_outline_storage;
const ExPolygons* efc_outline = nullptr;
@@ -390,17 +391,17 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
int32_t pt_x = scale_(pos.x());
int32_t pt_y = scale_(pos.y());
//ORCA: Snap painted ears to the EFC-adjusted outline when enabled.
//ORCA: Project painted ears to the EFC-adjusted outline when enabled.
if (use_efc_outline) {
if (efc_outline == nullptr) {
//ORCA: Compute EFC-adjusted outline lazily for painted ear snapping.
//ORCA: Compute the EFC-adjusted outline lazily for painted ear projection.
efc_outline_storage = get_print_object_bottom_layer_expolygons(*object);
efc_outline = &efc_outline_storage;
}
if (!efc_outline->empty()) {
Point closest_point;
//ORCA: Snap within the matching island to avoid drifting to another island.
//ORCA: Project within the matching island to avoid drifting to another island.
if (closest_point_on_matching_island(
raw_outline,
*efc_outline,

View File

@@ -147,6 +147,20 @@ Semver get_version_from_json(std::string file_path)
return Semver();
//throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file_path, err.what()));
}
catch(...) {
return Semver();
}
}
std::string get_vendor_cache_key(const std::string& json_path)
{
const Semver ver = get_version_from_json(json_path);
if (ver.valid())
return ver.to_string();
// No version field — use mtime as change fingerprint so edits invalidate the cache.
boost::system::error_code ec;
const std::time_t mtime = boost::filesystem::last_write_time(json_path, ec);
return ec ? std::string{} : ("mtime:" + std::to_string(mtime));
}
//BBS: add a function to load the key-values from xxx.json
@@ -742,6 +756,7 @@ void Preset::save(DynamicPrintConfig* parent_config)
idx_file.replace_extension(".info");
this->save_info(idx_file.string());
}
}
void Preset::reload(Preset const &parent)

View File

@@ -16,6 +16,14 @@
#include "Semver.hpp"
#include "ProjectTask.hpp"
#include <cereal/archives/binary.hpp>
#include <cereal/cereal.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/set.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
//BBS: change system directories
#define PRESET_SYSTEM_DIR "system"
#define PRESET_USER_DIR "user"
@@ -113,6 +121,10 @@ extern Semver get_version_from_json(std::string file_path);
//BBS: add a function to load the key-values from xxx.json
extern int get_values_from_json(std::string file_path, std::vector<std::string>& keys, std::map<std::string, std::string>& key_values);
// Returns the cache key for a vendor JSON: the Semver string for versioned
// vendors, or "mtime:<unix_timestamp>" for vendors without a version field.
extern std::string get_vendor_cache_key(const std::string& json_path);
extern ConfigFileType guess_config_file_type(const boost::property_tree::ptree &tree);
extern void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil_to_default, const DynamicPrintConfig& defaults);
@@ -130,6 +142,9 @@ public:
PrinterVariant() {}
PrinterVariant(const std::string &name) : name(name) {}
std::string name;
template<class Archive>
void serialize(Archive& ar) { ar(name); }
};
struct PrinterModel {
@@ -160,6 +175,15 @@ public:
}
const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
template<class Archive>
void serialize(Archive& ar)
{
ar(id, name, model_id, family, technology, variants, default_materials,
not_support_bed_types, bed_model, bed_texture, image_bed_type,
bottom_texture_end_name, use_double_extruder_default_texture,
bottom_texture_rect, middle_texture_rect, hotend_model);
}
};
std::vector<PrinterModel> models;
@@ -171,6 +195,13 @@ public:
bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); }
template<class Archive>
void serialize(Archive& ar)
{
ar(id, name, config_version, config_update_url, changelog_url,
models, default_filaments, default_sla_materials);
}
// Load VendorProfile from an ini file.
// If `load_all` is false, only the header with basic info (name, version, URLs) is loaded.
static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true);
@@ -418,12 +449,31 @@ public:
// BBS: move constructor to public
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
protected:
// Default constructor is public so cereal can default-construct elements when
// deserializing std::vector<Preset> (std::allocator is not a cereal::access friend).
Preset() = default;
protected:
friend class PresetCollection;
friend class PresetBundle;
friend class cereal::access;
// Serializes all value fields of Preset for the binary vendor cache.
// Raw pointers (vendor, loading_substitutions) are excluded — vendor is reconstructed
// by apply_vendor_cache() from the VendorProfile stored alongside the presets.
template<class Archive>
void serialize(Archive& ar)
{
ar(type, name, alias, file, version,
filament_id, setting_id, description,
renamed_from, is_system, is_visible,
is_default, is_external, is_dirty, is_compatible,
is_project_embedded, loaded,
m_from_orca_filament_lib, m_excluded_from,
bundle_id, user_id, base_id, sync_info,
updated_time, key_values, ini_str,
config);
}
};
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);

View File

@@ -1,7 +1,15 @@
#include <cassert>
#include <chrono>
#include <ctime>
#include <sstream>
#include "PresetBundle.hpp"
#include <boost/crc.hpp>
#include <cereal/archives/binary.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include "I18N.hpp"
@@ -520,6 +528,8 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward
//BBS: add config related logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id;
const auto startup_t0 = std::chrono::steady_clock::now();
//BBS: change system config to json
std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule);
@@ -545,6 +555,12 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward
set_calibrate_printer("");
{
const auto total_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - startup_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: all presets loaded in " << total_ms << " ms";
}
//BBS: add config related logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size();
return substitutions;
@@ -955,6 +971,8 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
bundles.m_bundles.clear();
bundles.WriteUnlock();
const auto user_load_t0 = std::chrono::steady_clock::now();
// Load bundle metadata from _local directory first
fs::path local_dir(folder / PRESET_LOCAL_DIR);
if (fs::exists(local_dir)) {
@@ -973,7 +991,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
metadata.filament_presets.clear();
metadata.printer_presets.clear();
// Add the profiles
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id));
@@ -1010,7 +1027,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
metadata.printer_presets.clear();
metadata.is_subscribed = true;
// Load presets from bundle (same logic as __local__)
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id));
@@ -1031,34 +1047,41 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
}
}
// BBS do not load sla_print
// BBS: change directoties by design
try {
std::string print_selected_preset_name = prints.get_selected_preset().name;
this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule);
prints.select_preset_by_name(print_selected_preset_name, false);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
// BBS: change directories by design
{
const auto json_t0 = std::chrono::steady_clock::now();
try {
std::string sel = prints.get_selected_preset().name;
this->prints.load_presets(dir_user_presets, PRESET_PRINT_NAME, substitutions, substitution_rule);
prints.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
try {
std::string sel = filaments.get_selected_preset().name;
this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule);
filaments.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
try {
std::string sel = printers.get_selected_preset().name;
this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule);
printers.select_preset_by_name(sel, false);
} catch (const std::runtime_error& err) { errors_cummulative += err.what(); }
if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative);
const auto json_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - json_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: user presets loaded from JSON in " << json_ms << " ms";
}
try {
std::string filament_selected_preset_name = filaments.get_selected_preset().name;
this->filaments.load_presets(dir_user_presets, PRESET_FILAMENT_NAME, substitutions, substitution_rule);
filaments.select_preset_by_name(filament_selected_preset_name, false);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
{
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - user_load_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: user + bundle presets loaded in " << ms << " ms";
}
try {
std::string printer_selected_preset_name = printers.get_selected_preset().name;
this->printers.load_presets(dir_user_presets, PRESET_PRINTER_NAME, substitutions, substitution_rule);
printers.select_preset_by_name(printer_selected_preset_name, false);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
}
if (!errors_cummulative.empty()) throw Slic3r::RuntimeError(errors_cummulative);
this->update_multi_material_filament_presets();
this->update_compatible(PresetSelectCompatibleType::Never);
set_calibrate_printer("");
return PresetsConfigSubstitutions();
@@ -2197,9 +2220,26 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
if (validation_mode)
dir = (boost::filesystem::path(data_dir())).make_preferred();
// Single-bundle cache: try user cache, then bundled cache, then JSON parse.
bool loaded_from_cache = false;
if (!validation_mode) {
const auto t0 = std::chrono::steady_clock::now();
const std::string expected_key = compute_system_presets_cache_key(dir.string());
if (try_load_system_presets_from_cache(expected_key)) {
update_system_maps();
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from single-bundle cache in " << ms << " ms";
return {PresetsConfigSubstitutions{}, ""};
}
loaded_from_cache = false; // cache miss — fall through to JSON parse
}
const auto json_load_t0 = std::chrono::steady_clock::now();
PresetsConfigSubstitutions substitutions;
std::string errors_cummulative;
bool first = true;
std::set<std::string> errored_vendors; // vendors whose JSON parse failed — skip their cache save
bool first = true;
std::vector<std::string> vendor_names;
// store all vendor names in vendor_names
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
@@ -2237,6 +2277,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
throw err;
errors_cummulative += err.what();
errors_cummulative += "\n";
errored_vendors.insert(orca_lib_vendor);
}
}
@@ -2273,6 +2314,7 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
throw std::runtime_error(parallel_errors[i]);
errors_cummulative += parallel_errors[i];
errors_cummulative += "\n";
errored_vendors.insert(other_vendors[i]);
continue;
}
if (!parallel_bundles[i])
@@ -2300,6 +2342,28 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
}
this->update_system_maps();
{
const auto json_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - json_load_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from JSON in " << json_ms << " ms";
}
// Save single-bundle cache after successful JSON parse.
if (!validation_mode && errored_vendors.empty()) {
const auto save_t0 = std::chrono::steady_clock::now();
const auto stats = save_system_presets_cache(dir.string(), user_system_presets_cache_path());
const auto save_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - save_t0).count();
if (stats.ok)
BOOST_LOG_TRIVIAL(info) << "PresetBundle: single-bundle cache saved in " << save_ms << " ms"
<< " (print=" << stats.print_presets
<< " filament=" << stats.filament_presets
<< " printer=" << stats.printer_presets << ")";
else
BOOST_LOG_TRIVIAL(warning) << "PresetBundle: single-bundle cache save failed";
}
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative;
return std::make_pair(std::move(substitutions), errors_cummulative);
@@ -5789,4 +5853,305 @@ bool BundleMetadata::save_to_json(const std::string& path) const
return false;
}
}
// ---- System presets single-bundle cache implementation ------------------
namespace {
#pragma pack(push, 1)
struct CacheFileHeader {
uint32_t magic;
uint32_t version;
uint64_t data_size;
uint32_t crc32;
};
#pragma pack(pop)
static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes");
// Presets for one vendor, grouped so vendor pointers can be re-wired on load.
struct VendorPresetGroup {
std::vector<Preset> prints, filaments, printers, sla_prints, sla_materials;
template<class Archive>
void serialize(Archive& ar) { ar(prints, filaments, printers, sla_prints, sla_materials); }
};
struct SystemPresetsCache {
static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
static constexpr uint32_t CACHE_VERSION = 1;
uint32_t cache_version = CACHE_VERSION;
uint32_t config_options_count = 0;
std::string bundle_key;
VendorMap vendors;
std::map<std::string, VendorPresetGroup> preset_groups;
std::map<std::string, DynamicPrintConfig> config_maps;
std::map<std::string, std::string> filament_id_maps;
template<class Archive>
void serialize(Archive& ar)
{
ar(cache_version, config_options_count, bundle_key,
vendors, preset_groups, config_maps, filament_id_maps);
}
bool is_valid(const std::string& expected_key) const
{
return cache_version == CACHE_VERSION
&& config_options_count == static_cast<uint32_t>(print_config_def.options.size())
&& bundle_key == expected_key;
}
};
} // anonymous namespace
// static
std::string PresetBundle::bundled_system_presets_cache_path()
{
return (boost::filesystem::path(resources_dir()) / "profiles" / "system_presets.cache")
.make_preferred().string();
}
// static
std::string PresetBundle::user_system_presets_cache_path()
{
return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / "system_presets.cache")
.make_preferred().string();
}
// static
std::string PresetBundle::compute_system_presets_cache_key(const std::string& system_dir)
{
// Sorted map so key is stable regardless of directory iteration order.
std::map<std::string, std::string> keys;
try {
for (const auto& e : boost::filesystem::directory_iterator(system_dir)) {
if (Slic3r::is_json_file(e.path().string()))
keys[e.path().stem().string()] = get_vendor_cache_key(e.path().string());
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot scan " << system_dir << ": " << ex.what();
}
std::string combined;
for (const auto& [name, key] : keys)
combined += name + ":" + key + ";";
return combined;
}
// static
bool PresetBundle::read_cache_blob(const std::string& path, std::string& out_blob)
{
try {
boost::nowide::ifstream ifs(path, std::ios::binary);
if (!ifs.is_open())
return false;
CacheFileHeader fhdr;
if (!ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)))
return false;
if (fhdr.magic != SystemPresetsCache::CACHE_MAGIC)
return false;
if (fhdr.data_size == 0 || fhdr.data_size > 512u * 1024u * 1024u)
return false;
out_blob.assign(fhdr.data_size, '\0');
if (!ifs.read(&out_blob[0], static_cast<std::streamsize>(fhdr.data_size)))
return false;
boost::crc_32_type crc;
crc.process_bytes(out_blob.data(), out_blob.size());
if (crc.checksum() != fhdr.crc32) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: CRC mismatch: " << path;
return false;
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: read failed (" << path << "): " << e.what();
return false;
}
}
// static
void PresetBundle::write_cache_blob(const std::string& path, const std::string& blob)
{
boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size());
try {
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
boost::nowide::ofstream ofs(path, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: cannot open for writing: " << path;
return;
}
CacheFileHeader fhdr;
fhdr.magic = SystemPresetsCache::CACHE_MAGIC;
fhdr.version = SystemPresetsCache::CACHE_VERSION;
fhdr.data_size = static_cast<uint64_t>(blob.size());
fhdr.crc32 = crc.checksum();
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: write failed (" << path << "): " << e.what();
}
}
// Apply one VendorPresetGroup from a loaded cache into this PresetBundle's collections.
static void apply_vendor_preset_group(VendorPresetGroup& grp,
const VendorProfile* vp,
PresetCollection& prints_coll,
PresetCollection& filaments_coll,
PrinterPresetCollection& printers_coll,
PresetCollection& sla_prints_coll,
PresetCollection& sla_materials_coll)
{
auto apply = [&](std::vector<Preset>& cached, PresetCollection& coll, bool is_filaments) {
for (Preset& cp : cached) {
// Reserve a slot in the collection, then move the fully-deserialized
// preset into it so all serialized fields are transferred at once.
// Only vendor (a raw pointer, excluded from serialization) is patched after.
DynamicPrintConfig config = cp.config;
Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), false, cp.version);
const std::string alias = cp.alias;
p = std::move(cp);
p.vendor = vp;
if (is_filaments)
coll.set_printer_hold_alias(alias, p);
}
};
apply(grp.prints, prints_coll, false);
apply(grp.filaments, filaments_coll, true);
apply(grp.printers, printers_coll, false);
apply(grp.sla_prints, sla_prints_coll, false);
apply(grp.sla_materials, sla_materials_coll, false);
}
bool PresetBundle::try_load_system_presets_from_cache(const std::string& expected_key)
{
auto try_path = [&](const std::string& path) -> bool {
std::string blob;
if (!read_cache_blob(path, blob))
return false;
try {
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
SystemPresetsCache cache;
ar(cache);
if (!cache.is_valid(expected_key))
return false;
this->reset(false);
vendors = std::move(cache.vendors);
for (auto& [vendor_id, grp] : cache.preset_groups) {
auto it = vendors.find(vendor_id);
if (it == vendors.end()) continue;
apply_vendor_preset_group(grp, &it->second,
prints, filaments, printers,
sla_prints, sla_materials);
}
m_config_maps = std::move(cache.config_maps);
m_filament_id_maps = std::move(cache.filament_id_maps);
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: apply failed (" << path << "): " << e.what();
return false;
}
};
if (try_path(user_system_presets_cache_path()))
return true;
if (try_path(bundled_system_presets_cache_path())) {
// Promote bundled cache to user path so future loads skip JSON parse.
const std::string user_path = user_system_presets_cache_path();
const std::string bundled_path = bundled_system_presets_cache_path();
try {
boost::filesystem::create_directories(
boost::filesystem::path(user_path).parent_path());
boost::filesystem::copy_file(bundled_path, user_path,
boost::filesystem::copy_options::overwrite_existing);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: promote failed: " << e.what();
}
return true;
}
return false;
}
PresetBundle::SaveCacheResult PresetBundle::save_system_presets_cache(
const std::string& profiles_dir, const std::string& output_path) const
{
SystemPresetsCache cache;
cache.config_options_count = static_cast<uint32_t>(print_config_def.options.size());
cache.bundle_key = compute_system_presets_cache_key(profiles_dir);
cache.vendors = vendors;
cache.config_maps = m_config_maps;
cache.filament_id_maps = m_filament_id_maps;
auto fill = [&](const PresetCollection& coll,
std::vector<Preset> VendorPresetGroup::*field) {
for (const Preset& p : coll())
if (p.is_system && p.vendor)
(cache.preset_groups[p.vendor->id].*field).push_back(p);
};
fill(prints, &VendorPresetGroup::prints);
fill(filaments, &VendorPresetGroup::filaments);
fill(sla_prints, &VendorPresetGroup::sla_prints);
fill(sla_materials, &VendorPresetGroup::sla_materials);
// PrinterPresetCollection is a PresetCollection subclass; iterate directly.
for (const Preset& p : printers())
if (p.is_system && p.vendor)
cache.preset_groups[p.vendor->id].printers.push_back(p);
std::ostringstream oss;
{ cereal::BinaryOutputArchive ar(oss); ar(cache); }
const std::string blob = oss.str();
write_cache_blob(output_path, blob);
SaveCacheResult result;
// Verify: read back and check validity.
if (std::string vblob; read_cache_blob(output_path, vblob)) {
try {
std::istringstream iss(vblob);
cereal::BinaryInputArchive ar(iss);
SystemPresetsCache v; ar(v);
result.ok = v.is_valid(cache.bundle_key);
} catch (...) {}
}
for (const auto& [vid, grp] : cache.preset_groups) {
result.print_presets += grp.prints.size();
result.filament_presets += grp.filaments.size();
result.printer_presets += grp.printers.size();
}
return result;
}
// static
bool PresetBundle::load_system_presets_cache_for_guide(const std::string& cache_path,
PresetBundle& out_bundle)
{
std::string blob;
if (!read_cache_blob(cache_path, blob))
return false;
try {
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
SystemPresetsCache cache;
ar(cache);
if (cache.cache_version != SystemPresetsCache::CACHE_VERSION)
return false;
out_bundle.vendors = std::move(cache.vendors);
for (auto& [vendor_id, grp] : cache.preset_groups) {
auto it = out_bundle.vendors.find(vendor_id);
if (it == out_bundle.vendors.end()) continue;
apply_vendor_preset_group(grp, &it->second,
out_bundle.prints,
out_bundle.filaments,
out_bundle.printers,
out_bundle.sla_prints,
out_bundle.sla_materials);
}
return !out_bundle.vendors.empty();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "SystemPresetsCache: guide load failed (" << cache_path << "): " << e.what();
return false;
}
}
} // namespace Slic3r

View File

@@ -13,6 +13,7 @@
#include <boost/filesystem/path.hpp>
#include <unordered_set>
#define DEFAULT_USER_FOLDER_NAME "default"
#define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json"
@@ -151,6 +152,32 @@ struct PresetBundleMetadata
class PresetBundle
{
public:
// ---- System presets single-bundle cache --------------------------------
// All vendor profiles + all system presets in one file.
// Bundled (CI): resources/profiles/system_presets.cache
// User runtime: data_dir/system/system_presets.cache
struct SaveCacheResult {
bool ok = false;
size_t print_presets = 0;
size_t filament_presets = 0;
size_t printer_presets = 0;
};
static std::string bundled_system_presets_cache_path();
static std::string user_system_presets_cache_path();
// Capture the currently-loaded PresetBundle and write a single-bundle cache
// to output_path. profiles_dir contains the vendor JSON files (for the key).
// Used by generate_system_cache and tests.
SaveCacheResult save_system_presets_cache(const std::string& profiles_dir,
const std::string& output_path) const;
// Load a single-bundle cache into out_bundle for the guide wizard.
// Returns false if the cache is missing, stale, or corrupt.
static bool load_system_presets_cache_for_guide(const std::string& cache_path,
PresetBundle& out_bundle);
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
Preset &in_print_preset,
const DynamicPrintConfig &project_config,
@@ -493,6 +520,15 @@ public:
bool check_preset_references() const;
private:
// Compute combined invalidation key from all vendor JSON files in system_dir.
static std::string compute_system_presets_cache_key(const std::string& system_dir);
// Try user cache then bundled cache; apply all presets on hit.
bool try_load_system_presets_from_cache(const std::string& expected_key);
// Read raw cache blob: verify magic, size, CRC.
static bool read_cache_blob(const std::string& path, std::string& out_blob);
// Write a cache blob with the standard 20-byte file header.
static void write_cache_blob(const std::string& path, const std::string& blob);
// Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match).
bool check_duplicate_filament_subtypes() const;

View File

@@ -2192,7 +2192,8 @@ namespace cereal {
archive(serialization_key_ordinal);
assert(serialization_key_ordinal > 0);
auto it = Slic3r::print_config_def.by_serialization_key_ordinal.find(serialization_key_ordinal);
assert(it != Slic3r::print_config_def.by_serialization_key_ordinal.end());
if (it == Slic3r::print_config_def.by_serialization_key_ordinal.end())
throw std::runtime_error("VendorCache: unknown serialization_key_ordinal " + std::to_string(serialization_key_ordinal) + " - cache is stale");
config.set_key_value(it->second->opt_key, it->second->load_option_from_archive(archive));
}
}

View File

@@ -190,6 +190,16 @@ public:
os << self.to_string();
return os;
}
// cereal: round-trip through the standard 3-part string (major.minor.patch).
// to_string() uses a BBS 4-part format that semver_parse() cannot read back.
template<class Archive>
std::string save_minimal(const Archive&) const { return to_string_sf(); }
template<class Archive>
void load_minimal(const Archive&, const std::string& s) {
if (auto v = Semver::parse(s)) *this = std::move(*v);
}
private:
semver_t ver;

View File

@@ -300,6 +300,10 @@ std::string header_gcodeviewer_generated();
// getpid platform wrapper
extern unsigned get_current_pid();
// Per-user id for isolating temp dirs; empty on Windows (its temp dir is already per-user).
std::string per_user_temp_id();
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
// BBS: backup & restore
std::string get_process_name(int pid);

View File

@@ -1285,6 +1285,24 @@ unsigned get_current_pid()
#endif
}
std::string per_user_temp_id()
{
#ifdef WIN32
return {};
#else
return std::to_string(static_cast<unsigned long>(::getuid()));
#endif
}
std::string per_user_temp_dir(const std::string &base, const std::string &user_id)
{
if (user_id.empty())
return base;
// Keep the id at the top level so each user's dir sits directly in the world-writable temp
// root; a shared parent dir would be owned by whichever user created it first.
return base + "/orcaslicer_" + user_id;
}
// BBS: backup & restore
std::string get_process_name(int pid)
{

View File

@@ -2,6 +2,7 @@
#include "ConfigWizard.hpp"
#include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/iostreams/detail/select.hpp>
#include <boost/log/trivial.hpp>
@@ -9,6 +10,7 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/GUI_App.hpp"
@@ -41,7 +43,32 @@ using namespace nlohmann;
namespace Slic3r { namespace GUI {
json m_ProfileJson;
static std::string guide_json_cache_version_key(const boost::filesystem::path& rsrc_dir,
const boost::filesystem::path& user_dir)
{
std::vector<std::string> parts;
auto collect = [&](const boost::filesystem::path& dir) {
boost::system::error_code ec;
if (!boost::filesystem::exists(dir, ec)) return;
for (const auto& e : boost::filesystem::directory_iterator(dir, ec)) {
if (e.path().extension().string() != ".json") continue;
const std::string k = get_vendor_cache_key(e.path().string());
if (!k.empty())
parts.push_back(e.path().filename().string() + "=" + k);
}
};
collect(rsrc_dir);
if (user_dir != rsrc_dir) collect(user_dir);
std::sort(parts.begin(), parts.end());
std::string result;
for (const auto& p : parts) { result += p; result += ';'; }
return result;
}
static boost::filesystem::path guide_json_cache_path()
{
return boost::filesystem::path(data_dir()) / "guide_profile_cache.json";
}
static wxString update_custom_filaments()
{
@@ -191,11 +218,10 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style)
GuideFrame::~GuideFrame()
{
m_destroy = true;
if (m_load_task && m_load_task->joinable()) {
*m_cancel_token = true; // signal any queued CallAfter lambdas before join
if (m_load_task && m_load_task->joinable())
m_load_task->join();
delete m_load_task;
m_load_task = nullptr;
}
m_load_task.reset();
if (m_browser) {
delete m_browser;
m_browser = nullptr;
@@ -301,15 +327,85 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt)
/**
* Callback invoked when a navigation request was accepted
*/
void GuideFrame::init_guide_paths()
{
m_ProfileJson = json::parse("{}");
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
orca_bundle_rsrc = true;
if (boost::filesystem::exists(vendor_dir)) {
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
if (!boost::filesystem::is_directory(entry) &&
boost::iequals(entry.path().extension().string(), ".json") &&
!boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
orca_bundle_rsrc = false;
break;
}
}
}
auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json)
? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string()
: (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
}
void GuideFrame::on_profile_loaded()
{
// Must be called on the main thread.
SaveProfileData();
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
json res;
res["command"] = "userguide_profile_load_finish";
res["sequence_id"] = "10001";
RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true)));
}
void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt)
{
//wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'");
if (!bFirstComplete) {
m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this));
// boost::thread LoadProfileThread(boost::bind(&GuideFrame::LoadProfileData, this));
//LoadProfileThread.detach();
bFirstComplete = true;
try {
init_guide_paths();
if (BuildProfileDataFromPresetBundle()) {
// Persist so future opens that start before preset_bundle is ready
// can skip the slower loading paths. Capture by value so the
// thread is safe even if the dialog closes before it finishes.
boost::thread([data = m_ProfileJson,
rsrc = rsrc_vendor_dir,
user = vendor_dir] {
try {
json cache;
cache["version"] = guide_json_cache_version_key(rsrc, user);
if (cache["version"].get<std::string>().empty()) return;
json base = data;
for (auto& entry : base["model"]) entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}).detach();
if (!m_destroy)
on_profile_loaded();
} else {
// Presets not yet in memory — delegate to background thread.
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what();
m_load_task = std::make_unique<boost::thread>(boost::bind(&GuideFrame::LoadProfileData, this));
}
}
m_browser->Show();
@@ -1127,99 +1223,355 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
return status;
}
int GuideFrame::LoadProfileData()
bool GuideFrame::TryLoadGuideJsonCache()
{
const auto path = guide_json_cache_path();
boost::system::error_code ec;
if (!boost::filesystem::exists(path, ec)) return false;
try {
boost::nowide::ifstream ifs(path.string());
json cache;
ifs >> cache;
if (!cache.contains("version") || !cache.contains("data")) return false;
const std::string expected = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (expected.empty() || cache["version"].get<std::string>() != expected) return false;
m_ProfileJson = cache["data"];
if (m_ProfileJson["machine"].empty()) return false;
BOOST_LOG_TRIVIAL(info) << "GuideFrame: loaded profile data from guide JSON cache ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: guide JSON cache load failed: " << e.what();
return false;
}
}
void GuideFrame::SaveGuideJsonCache()
{
try {
m_ProfileJson = json::parse("{}");
json cache;
cache["version"] = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (cache["version"].get<std::string>().empty()) return;
json base = m_ProfileJson;
// Strip user-specific state — SaveProfileData() re-applies it from AppConfig.
for (auto& entry : base["model"])
entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}
bool GuideFrame::BuildProfileDataFromPresetBundle()
{
PresetBundle* pb = wxGetApp().preset_bundle;
if (!pb || pb->vendors.empty())
return false;
try {
// Models from vendor profiles
for (const auto& [vendor_id, vp] : pb->vendors) {
for (const auto& model : vp.models) {
std::string nozzle_str;
for (const auto& v : model.variants) {
if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name;
}
std::string materials_str;
for (const auto& m : model.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vendor_id / (model.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (model.id + "_cover.png"))
.make_preferred();
json entry;
entry["model"] = model.id;
entry["name"] = model.name;
entry["vendor"] = vendor_id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
entry["nozzle_selected"] = "";
entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry);
}
}
// Machine map: preset name -> {model, nozzle variant}
for (const Preset& p : pb->printers()) {
if (!p.is_system) continue;
const auto* printer_model = p.config.option<ConfigOptionString>("printer_model");
const auto* printer_variant = p.config.option<ConfigOptionString>("printer_variant");
if (!printer_model || printer_model->value.empty() || !printer_variant) continue;
json mach;
mach["model"] = printer_model->value;
mach["nozzle"] = printer_variant->value;
m_ProfileJson["machine"][p.name] = mach;
}
// Filament map from system filament presets (vendor/type already resolved in config)
for (const Preset& p : pb->filaments()) {
if (!p.is_system) continue;
const auto* fila_vendor = p.config.option<ConfigOptionStrings>("filament_vendor");
const auto* fila_type = p.config.option<ConfigOptionStrings>("filament_type");
const auto* compat_printers = p.config.option<ConfigOptionStrings>("compatible_printers");
std::string vendor = (fila_vendor && !fila_vendor->values.empty()) ? fila_vendor->values[0] : "";
std::string type = (fila_type && !fila_type->values.empty()) ? fila_type->values[0] : "";
std::string model_list;
if (compat_printers) {
for (const std::string& pname : compat_printers->values) {
if (m_ProfileJson["machine"].contains(pname)) {
std::string m = m_ProfileJson["machine"][pname]["model"];
std::string n = m_ProfileJson["machine"][pname]["nozzle"];
model_list += "[" + m + "++" + n + "]";
}
}
}
json ff;
ff["name"] = p.name;
ff["sub_path"] = p.file;
ff["vendor"] = vendor;
ff["type"] = type;
ff["models"] = model_list;
ff["selected"] = 0;
m_ProfileJson["filament"][p.name] = ff;
}
// Process list from visible system print presets
for (const Preset& p : pb->prints()) {
if (!p.is_system || !p.is_visible) continue;
json entry;
entry["name"] = p.name;
entry["sub_path"] = p.file;
m_ProfileJson["process"].push_back(entry);
}
// If rsrc_vendor_dir has vendor JSONs not covered by the current bundle, the
// bundle is incomplete (e.g. dev env where data_dir/system only has
// OrcaFilamentLibrary+Custom). Fall back so LoadProfileFamily reads both dirs.
try {
for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) {
if (e.path().extension().string() != ".json") continue;
const std::string stem = e.path().stem().string();
if (pb->vendors.find(stem) == pb->vendors.end()) {
BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << stem
<< "' in resources but not in preset_bundle — falling back to JSON loading";
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false;
}
}
} catch (const std::exception&) {}
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from preset_bundle ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromPresetBundle failed: " << e.what()
<< " — falling back to JSON loading";
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false;
}
}
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
// Builds guide profile JSON from the per-vendor bundled caches
// (resources/profiles/system_presets.cache, generated by CI).
// This avoids the 90-second LoadProfileFamily fallback on first launch.
bool GuideFrame::BuildProfileDataFromBundledCache()
{
const std::string cache_path = PresetBundle::bundled_system_presets_cache_path();
if (!boost::filesystem::exists(cache_path))
return false;
// Orca: add custom as default
// Orca: add json logic for vendor bundle
orca_bundle_rsrc = true;
try {
PresetBundle bundle;
if (!PresetBundle::load_system_presets_cache_for_guide(cache_path, bundle))
return false;
// search if there exists a .json file in vendor_dir folder, if exists, set orca_bundle_rsrc to false
for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) {
if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) {
orca_bundle_rsrc = false;
break;
for (const auto& [vendor_id, vp] : bundle.vendors) {
for (const auto& cm : vp.models) {
std::string nozzle_str;
for (const auto& v : cm.variants) {
if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name;
}
std::string materials_str;
for (const auto& m : cm.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (cm.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (cm.id + "_cover.png"))
.make_preferred();
json entry;
entry["model"] = cm.id;
entry["name"] = cm.name;
entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
entry["nozzle_selected"] = "";
entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry);
}
}
// load the default filament library first
std::set<std::string> loaded_vendors;
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
if (boost::filesystem::exists(vendor_dir / filament_library_name)) {
m_OrcaFilaLibPath = (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
} else {
m_OrcaFilaLibPath = (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string();
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
}
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
for (const Preset& cp : bundle.printers()) {
if (!cp.is_system || !cp.vendor) continue;
const auto* pm = cp.config.option<ConfigOptionString>("printer_model");
const auto* pv = cp.config.option<ConfigOptionString>("printer_variant");
if (!pm || pm->value.empty() || !pv) continue;
//load custom bundle from user data path
boost::filesystem::directory_iterator endIter;
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if(strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy)
return 0;
json mach;
mach["model"] = pm->value;
mach["nozzle"] = pv->value;
m_ProfileJson["machine"][cp.name] = mach;
}
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
for (const Preset& cp : bundle.filaments()) {
if (!cp.is_system || !cp.vendor) continue;
const auto* fv = cp.config.option<ConfigOptionStrings>("filament_vendor");
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
const auto* compat = cp.config.option<ConfigOptionStrings>("compatible_printers");
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : "";
std::string type = (ft && !ft->values.empty()) ? ft->values[0] : "";
std::string model_list;
if (compat) {
for (const std::string& pname : compat->values) {
if (m_ProfileJson["machine"].contains(pname)) {
std::string m = m_ProfileJson["machine"][pname]["model"];
std::string n = m_ProfileJson["machine"][pname]["nozzle"];
model_list += "[" + m + "++" + n + "]";
}
}
}
if (m_destroy)
return 0;
json ff;
ff["name"] = cp.name;
ff["sub_path"] = cp.file;
ff["vendor"] = vendor;
ff["type"] = type;
ff["models"] = model_list;
ff["selected"] = 0;
m_ProfileJson["filament"][cp.name] = ff;
}
wxGetApp().CallAfter([this] {
if (!m_destroy) {
//sync to appconfig first to populate current selections
SaveProfileData();
for (const Preset& cp : bundle.prints()) {
if (!cp.is_system || !cp.vendor || !cp.is_visible) continue;
json entry;
entry["name"] = cp.name;
entry["sub_path"] = cp.file;
m_ProfileJson["process"].push_back(entry);
}
//sync to web after selections are populated
std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from single-bundle cache ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache failed: " << ex.what();
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return false;
}
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents: " << std::endl << strAll;
json m_Res = json::object();
m_Res["command"] = "userguide_profile_load_finish";
m_Res["sequence_id"] = "10001";
wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true));
int GuideFrame::LoadProfileData()
{
// Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded).
// Loading order (fastest to slowest):
// 1. Guide JSON cache (data_dir/guide_profile_cache.json, sub-second)
// 2. Bundled per-vendor binary caches (CI-generated, ~1-2s)
// 3. Read all vendor JSONs (~90s)
// After paths 2 or 3 the guide JSON cache is written so next open uses path 1.
try {
if (!TryLoadGuideJsonCache()) {
if (!BuildProfileDataFromBundledCache()) {
// Last resort — read all vendor JSONs (~90s)
std::set<std::string> loaded_vendors;
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
if (boost::filesystem::exists(vendor_dir / filament_library_name))
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
else
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
RunScript(strJS);
boost::filesystem::directory_iterator endIter;
for (boost::filesystem::directory_iterator iter(vendor_dir); iter != endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy) return 0;
}
boost::filesystem::directory_iterator others_endIter;
for (boost::filesystem::directory_iterator iter(rsrc_vendor_dir); iter != others_endIter; iter++) {
if (!boost::filesystem::is_directory(*iter)) {
wxString strVendor = from_u8(iter->path().string()).BeforeLast('.');
strVendor = strVendor.AfterLast('\\');
strVendor = strVendor.AfterLast('/');
wxString strExtension = from_u8(iter->path().string()).AfterLast('.').Lower();
if (strExtension.CmpNoCase("json") != 0 || loaded_vendors.find(w2s(strVendor)) != loaded_vendors.end())
continue;
LoadProfileFamily(w2s(strVendor), iter->path().string());
loaded_vendors.insert(w2s(strVendor));
}
if (m_destroy) return 0;
}
}
// Persist the result so subsequent opens skip both the bundled cache and
// the slow JSON loading path entirely.
SaveGuideJsonCache();
}
// Capture the cancel token by value (shared_ptr) so the lambda doesn't
// touch `this` if GuideFrame is destroyed before the event fires.
auto tok = m_cancel_token;
wxGetApp().CallAfter([this, tok] {
if (!*tok)
on_profile_loaded();
});
} catch (std::exception& e) {
// wxLogMessage("GUIDE: load_profile_error %s ", e.what());
// wxMessageBox(e.what(), "", MB_OK);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what() << std::endl;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what();
}
filament_info_cache.clear();

View File

@@ -30,10 +30,14 @@
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include <atomic>
#include <memory>
#include <unordered_map>
#include <nlohmann/json.hpp>
#include <boost/thread.hpp>
namespace Slic3r { namespace GUI {
class GuideFrame : public DPIDialog
@@ -78,6 +82,12 @@ public:
int LoadProfileData();
int SaveProfileData();
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
void init_guide_paths();
void on_profile_loaded();
bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromBundledCache();
bool TryLoadGuideJsonCache();
void SaveGuideJsonCache();
int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
@@ -112,8 +122,11 @@ private:
//First Load
bool bFirstComplete{false};
bool m_destroy{false};
boost::thread* m_load_task{ nullptr };
std::atomic<bool> m_destroy{false};
// Shared cancel token captured by CallAfter lambdas so they don't touch
// `this` after the destructor has run and the object is freed.
std::shared_ptr<std::atomic<bool>> m_cancel_token{std::make_shared<std::atomic<bool>>(false)};
std::unique_ptr<boost::thread> m_load_task;
// User Config
bool PrivacyUse;
@@ -123,6 +136,7 @@ private:
bool InstallNetplugin;
bool network_plugin_ready {false};
json m_ProfileJson;
json m_OrcaFilaList;
std::string m_OrcaFilaLibPath;

View File

@@ -436,7 +436,9 @@ wxBitmap create_scaled_bitmap( const std::string& bmp_name_in,
return create_scaled_bitmap2(bmp_name_in, cache, win, px_cnt, grayscale, resize, array_new_color);
}
unsigned int width = 0;
unsigned int height = (unsigned int) (win->FromDIP(px_cnt) + 0.5f);
// win may be nullptr; use the static overload, which falls back to the primary display DPI.
// Calling win->FromDIP() on a null win is UB and lets the optimizer drop later null checks.
unsigned int height = (unsigned int) (wxWindow::FromDIP(px_cnt, win) + 0.5f);
std::string bmp_name = bmp_name_in;
boost::replace_last(bmp_name, ".png", "");
@@ -450,7 +452,7 @@ wxBitmap create_scaled_bitmap( const std::string& bmp_name_in,
// Try loading an SVG first, then PNG if SVG was not found:
wxBitmap *bmp = cache.load_svg(bmp_name, width, height, grayscale, dark_mode, new_color, resize ? em_unit(win) * 0.1f : 0.f);
if (bmp == nullptr) {
bmp = cache.load_png(bmp_name, width, height, grayscale, resize ? win->FromDIP(10) * 0.1f : 0.f);
bmp = cache.load_png(bmp_name, width, height, grayscale, resize ? wxWindow::FromDIP(10, win) * 0.1f : 0.f);
}
if (bmp == nullptr) {
@@ -471,7 +473,8 @@ wxBitmap create_scaled_bitmap2(const std::string& bmp_name_in, Slic3r::GUI::Bitm
const vector<std::string>& array_new_color/* = vector<std::string>()*/) // color witch will used instead of orange
{
unsigned int width = 0;
unsigned int height = (unsigned int)(win->FromDIP(px_cnt) + 0.5f);
// win may be nullptr; see create_scaled_bitmap() above.
unsigned int height = (unsigned int)(wxWindow::FromDIP(px_cnt, win) + 0.5f);
std::string bmp_name = bmp_name_in;
boost::replace_last(bmp_name, ".png", "");

View File

@@ -14,6 +14,7 @@ add_executable(${_TEST_NAME}_tests
test_config.cpp
test_preset_bundle_loading.cpp
test_preset_setting_id.cpp
test_vendor_cache.cpp
test_elephant_foot_compensation.cpp
test_geometry.cpp
test_placeholder_parser.cpp
@@ -23,6 +24,7 @@ add_executable(${_TEST_NAME}_tests
test_stl.cpp
test_meshboolean.cpp
test_marchingsquares.cpp
test_utils.cpp
test_timeutils.cpp
test_voronoi.cpp
test_optimizers.cpp

View File

@@ -0,0 +1,54 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/Utils.hpp"
#ifndef _WIN32
#include <unistd.h> // getuid
#endif
using namespace Slic3r;
TEST_CASE("per_user_temp_dir composes a per-user temp root", "[utils]") {
const std::string base = "/tmp";
SECTION("an empty id returns base unchanged") {
REQUIRE(per_user_temp_dir(base, "") == base);
}
SECTION("a non-empty id is appended at the top level") {
REQUIRE(per_user_temp_dir(base, "1000") == base + "/orcaslicer_1000");
}
SECTION("distinct ids produce distinct roots") {
REQUIRE(per_user_temp_dir(base, "1000") != per_user_temp_dir(base, "1001"));
}
}
TEST_CASE("per_user_temp_id follows the platform contract", "[utils]") {
const std::string id = per_user_temp_id();
SECTION("stable across calls") {
REQUIRE(per_user_temp_id() == id);
}
#ifdef _WIN32
SECTION("empty on Windows (its temp dir is already per-user)") {
REQUIRE(id.empty());
}
#else
SECTION("the current uid on Linux/macOS") {
REQUIRE_FALSE(id.empty());
REQUIRE(id == std::to_string(static_cast<unsigned long>(::getuid())));
}
#endif
}
// The end-to-end contract callers depend on: the temp root is left alone on
// Windows and isolated per user on Linux/macOS.
TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[utils]") {
const std::string base = "/tmp";
const std::string root = per_user_temp_dir(base, per_user_temp_id());
#ifdef _WIN32
REQUIRE(root == base);
#else
REQUIRE(root != base);
REQUIRE_THAT(root, Catch::Matchers::StartsWith(base + "/orcaslicer_"));
#endif
}

View File

@@ -0,0 +1,458 @@
#include <catch2/catch_all.hpp>
#include <boost/filesystem.hpp>
#include <boost/crc.hpp>
#include <fstream>
#include <set>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PrintConfig.hpp"
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace {
struct TempDir {
fs::path path;
TempDir() {
path = fs::temp_directory_path() / fs::unique_path("orca-cache-test-%%%%-%%%%");
fs::create_directories(path);
}
~TempDir() { boost::system::error_code ec; fs::remove_all(path, ec); }
};
std::string write_vendor_json(const fs::path& dir, const std::string& vendor_id,
const std::string& version = "1.0.0")
{
const fs::path p = dir / (vendor_id + ".json");
std::ofstream f(p.string());
f << R"({"version":")" << version << R"(","name":")" << vendor_id << R"("})";
return p.string();
}
std::string write_versionless_vendor_json(const fs::path& dir, const std::string& vendor_id)
{
const fs::path p = dir / (vendor_id + ".json");
std::ofstream f(p.string());
f << R"({"name":")" << vendor_id << R"("})";
return p.string();
}
void corrupt_blob_byte(const std::string& path)
{
std::fstream f(path, std::ios::in | std::ios::out | std::ios::binary);
f.seekp(30);
char b = 0; f.read(&b, 1);
f.seekp(30);
b ^= 0xFF;
f.write(&b, 1);
}
// Patch cache_version (blob[0..3]) and recompute CRC so the file passes
// the CRC check but fails the cache_version check in load_system_presets_cache_for_guide.
void patch_cache_version(const std::string& path, uint32_t wrong_version)
{
std::ifstream in(path, std::ios::binary);
std::vector<char> data(std::istreambuf_iterator<char>(in), {});
in.close();
if (data.size() < 24) return;
std::memcpy(&data[20], &wrong_version, 4);
boost::crc_32_type crc;
crc.process_bytes(&data[20], data.size() - 20);
const uint32_t new_crc = crc.checksum();
std::memcpy(&data[16], &new_crc, 4);
std::ofstream out(path, std::ios::binary | std::ios::trunc);
out.write(data.data(), static_cast<std::streamsize>(data.size()));
}
void add_vendor(PresetBundle& bundle, const std::string& vendor_id,
const std::string& name = "", Semver ver = Semver(1, 0, 0))
{
VendorProfile vp(vendor_id);
vp.name = name.empty() ? vendor_id + " Corp" : name;
vp.config_version = ver;
bundle.vendors.emplace(vendor_id, vp);
}
Preset& add_system_preset(PresetCollection& coll, const std::string& name,
const VendorProfile* vp)
{
Preset& p = coll.load_preset("", name, DynamicPrintConfig(coll.default_preset().config), false);
p.is_system = true;
p.vendor = vp;
return p;
}
PresetBundle::SaveCacheResult save_one_vendor(PresetBundle& src,
const fs::path& profiles_dir,
const fs::path& cache_path)
{
return src.save_system_presets_cache(profiles_dir.string(), cache_path.string());
}
// Helper: filter a collection by vendor_id.
std::vector<const Preset*> presets_for(const PresetCollection& coll, const std::string& vendor_id)
{
std::vector<const Preset*> out;
for (const Preset& p : coll())
if (p.is_system && p.vendor && p.vendor->id == vendor_id)
out.push_back(&p);
return out;
}
} // namespace
TEST_CASE("SystemPresetsCache: save and load via guide API", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", vp);
fp.alias = "Acme PLA";
fp.filament_id = "GFL_acme_pla";
add_system_preset(src.printers, vid + " Printer 0.4", vp);
const auto stats = save_one_vendor(src, tmp.path, cache);
REQUIRE(stats.ok);
CHECK(stats.filament_presets == 1);
CHECK(stats.printer_presets == 1);
CHECK(stats.print_presets == 0);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
REQUIRE(out.vendors.count(vid) == 1);
auto fi = presets_for(out.filaments, vid);
auto pr = presets_for(out.printers, vid);
REQUIRE(fi.size() == 1);
CHECK(fi[0]->name == vid + " PLA @0.4");
CHECK(fi[0]->alias == "Acme PLA");
CHECK(fi[0]->filament_id == "GFL_acme_pla");
REQUIRE(pr.size() == 1);
CHECK(pr[0]->name == vid + " Printer 0.4");
}
TEST_CASE("SystemPresetsCache: missing file returns false", "[VendorCache]")
{
TempDir tmp;
PresetBundle out;
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(
(tmp.path / "nonexistent.cache").string(), out));
}
TEST_CASE("SystemPresetsCache: corrupt data rejected by CRC check", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
save_one_vendor(src, tmp.path, cache);
corrupt_blob_byte(cache.string());
PresetBundle out;
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
}
TEST_CASE("SystemPresetsCache: multiple vendors in one bundle are isolated", "[VendorCache]")
{
TempDir tmp;
const std::string vid1 = "VendorA";
const std::string vid2 = "VendorB";
write_vendor_json(tmp.path, vid1);
write_vendor_json(tmp.path, vid2);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
for (const auto& vid : {vid1, vid2}) {
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
add_system_preset(src.filaments, vid + " PLA", vp);
add_system_preset(src.printers, vid + " Printer", vp);
}
REQUIRE(save_one_vendor(src, tmp.path, cache).ok);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
REQUIRE(out.vendors.count(vid1) == 1);
REQUIRE(out.vendors.count(vid2) == 1);
for (const auto& vid : {vid1, vid2}) {
auto fi = presets_for(out.filaments, vid);
auto pr = presets_for(out.printers, vid);
REQUIRE(fi.size() == 1);
CHECK(fi[0]->name == vid + " PLA");
REQUIRE(pr.size() == 1);
CHECK(pr[0]->name == vid + " Printer");
}
}
TEST_CASE("SystemPresetsCache: vendor profile fields are preserved", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid, "2.5.1");
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
VendorProfile vp(vid);
vp.name = "Acme Corporation";
vp.config_version = Semver(2, 5, 1);
VendorProfile::PrinterModel model;
model.id = "AcmePro";
model.name = "Acme Pro";
VendorProfile::PrinterVariant v0_4; v0_4.name = "0.4";
model.variants.push_back(v0_4);
vp.models.push_back(model);
src.vendors.emplace(vid, vp);
save_one_vendor(src, tmp.path, cache);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
REQUIRE(out.vendors.count(vid) == 1);
const VendorProfile& gvp = out.vendors.at(vid);
CHECK(gvp.id == vid);
CHECK(gvp.name == "Acme Corporation");
REQUIRE(gvp.models.size() == 1);
CHECK(gvp.models[0].id == "AcmePro");
CHECK(gvp.models[0].name == "Acme Pro");
REQUIRE(gvp.models[0].variants.size() == 1);
CHECK(gvp.models[0].variants[0].name == "0.4");
}
TEST_CASE("SystemPresetsCache: config option values are preserved", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
Preset& fp = add_system_preset(src.filaments, vid + " PETG @0.4", &src.vendors.at(vid));
fp.config.set_key_value("filament_type", new ConfigOptionStrings({"PETG"}));
save_one_vendor(src, tmp.path, cache);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
auto fi = presets_for(out.filaments, vid);
REQUIRE(fi.size() == 1);
const auto* ft = fi[0]->config.option<ConfigOptionStrings>("filament_type");
REQUIRE(ft != nullptr);
REQUIRE(ft->values.size() >= 1);
CHECK(ft->values[0] == "PETG");
}
TEST_CASE("SystemPresetsCache: multiple presets per collection round-trip", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
const VendorProfile* vp = &src.vendors.at(vid);
const std::vector<std::string> fi_names = {vid + " PLA", vid + " PETG", vid + " ABS"};
const std::vector<std::string> pr_names = {vid + " Printer 0.4", vid + " Printer 0.6"};
for (const auto& n : fi_names) add_system_preset(src.filaments, n, vp);
for (const auto& n : pr_names) add_system_preset(src.printers, n, vp);
const auto stats = save_one_vendor(src, tmp.path, cache);
REQUIRE(stats.ok);
CHECK(stats.filament_presets == 3);
CHECK(stats.printer_presets == 2);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
auto fi = presets_for(out.filaments, vid);
auto pr = presets_for(out.printers, vid);
REQUIRE(fi.size() == 3);
REQUIRE(pr.size() == 2);
std::set<std::string> fi_got, pr_got;
for (const auto* p : fi) fi_got.insert(p->name);
for (const auto* p : pr) pr_got.insert(p->name);
for (const auto& n : fi_names) CHECK(fi_got.count(n) == 1);
for (const auto& n : pr_names) CHECK(pr_got.count(n) == 1);
}
TEST_CASE("SystemPresetsCache: truncated file is rejected", "[VendorCache]")
{
TempDir tmp;
const fs::path cache = tmp.path / "truncated.cache";
{
std::ofstream f(cache.string(), std::ios::binary);
const char data[] = {0x4F, 0x52, 0x43};
f.write(data, sizeof(data));
}
PresetBundle out;
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
}
TEST_CASE("SystemPresetsCache: wrong magic is rejected", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
save_one_vendor(src, tmp.path, cache);
{
std::fstream f(cache.string(), std::ios::in | std::ios::out | std::ios::binary);
const uint32_t bad = 0xDEADBEEFu;
f.write(reinterpret_cast<const char*>(&bad), sizeof(bad));
}
PresetBundle out;
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
}
TEST_CASE("SystemPresetsCache: vendor with no presets saves and loads cleanly", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
VendorProfile vp(vid);
vp.name = "Acme Corporation";
vp.config_version = Semver(1, 0, 0);
src.vendors.emplace(vid, vp);
const auto stats = save_one_vendor(src, tmp.path, cache);
REQUIRE(stats.ok);
CHECK(stats.print_presets == 0);
CHECK(stats.filament_presets == 0);
CHECK(stats.printer_presets == 0);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
REQUIRE(out.vendors.count(vid) == 1);
CHECK(out.vendors.at(vid).id == vid);
CHECK(out.vendors.at(vid).name == "Acme Corporation");
CHECK(presets_for(out.filaments, vid).empty());
CHECK(presets_for(out.printers, vid).empty());
CHECK(presets_for(out.prints, vid).empty());
}
TEST_CASE("SystemPresetsCache: all Preset metadata fields are preserved", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
Preset& fp = add_system_preset(src.filaments, vid + " PLA @0.4", &src.vendors.at(vid));
fp.setting_id = "sid-test-001";
fp.description = "A test filament preset";
fp.bundle_id = "bundle-xyz";
fp.user_id = "user-abc";
fp.base_id = "base-123";
fp.sync_info = "update";
fp.updated_time = 1700000000LL;
fp.key_values = {{"color", "red"}, {"diameter", "1.75"}};
fp.ini_str = "[filament]\nnozzle_temperature = 230\n";
save_one_vendor(src, tmp.path, cache);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
auto fi = presets_for(out.filaments, vid);
REQUIRE(fi.size() == 1);
CHECK(fi[0]->setting_id == "sid-test-001");
CHECK(fi[0]->description == "A test filament preset");
CHECK(fi[0]->bundle_id == "bundle-xyz");
CHECK(fi[0]->user_id == "user-abc");
CHECK(fi[0]->base_id == "base-123");
CHECK(fi[0]->sync_info == "update");
CHECK(fi[0]->updated_time == 1700000000LL);
REQUIRE(fi[0]->key_values.count("color") == 1);
CHECK(fi[0]->key_values.at("color") == "red");
REQUIRE(fi[0]->key_values.count("diameter") == 1);
CHECK(fi[0]->key_values.at("diameter") == "1.75");
CHECK(fi[0]->ini_str == "[filament]\nnozzle_temperature = 230\n");
}
TEST_CASE("SystemPresetsCache: wrong cache_version is rejected", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
save_one_vendor(src, tmp.path, cache);
patch_cache_version(cache.string(), 0xFFFFFFFFu);
PresetBundle out;
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
}
TEST_CASE("SystemPresetsCache: mid-blob truncation is rejected", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
save_one_vendor(src, tmp.path, cache);
{
std::ifstream in(cache.string(), std::ios::binary);
std::vector<char> buf(30); // 20-byte header + 10 bytes of blob
in.read(buf.data(), 30);
in.close();
std::ofstream out(cache.string(), std::ios::binary | std::ios::trunc);
out.write(buf.data(), 30);
}
PresetBundle out;
CHECK_FALSE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
}
TEST_CASE("SystemPresetsCache: versionless vendor uses mtime key in bundle", "[VendorCache]")
{
TempDir tmp;
const std::string vid = "Acme";
write_versionless_vendor_json(tmp.path, vid);
const fs::path cache = tmp.path / "system_presets.cache";
PresetBundle src;
add_vendor(src, vid);
add_system_preset(src.filaments, vid + " PLA", &src.vendors.at(vid));
REQUIRE(save_one_vendor(src, tmp.path, cache).ok);
PresetBundle out;
REQUIRE(PresetBundle::load_system_presets_cache_for_guide(cache.string(), out));
auto fi = presets_for(out.filaments, vid);
REQUIRE(fi.size() == 1);
CHECK(fi[0]->name == vid + " PLA");
}