Compare commits

...

20 Commits

Author SHA1 Message Date
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
13 changed files with 1183 additions and 115 deletions

View File

@@ -117,6 +117,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 }}
@@ -292,6 +301,22 @@ jobs:
# WindowsSDKVersion: '10.0.26100.0\'
run: .\build_release_vs.bat slicer
- name: Generate system presets cache (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$tool = Get-ChildItem -Recurse -Path build -Filter "generate_system_cache.exe" | Select-Object -First 1
if (-not $tool) { Write-Error "generate_system_cache.exe not found in build tree"; exit 1 }
$profiles = Get-ChildItem -Recurse -Path build -Directory -Filter profiles |
Where-Object { $_.FullName -match 'resources' } | Select-Object -First 1
if (-not $profiles) { Write-Error "profiles directory not found in build tree"; 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 build -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
- name: Create installer Win
if: runner.os == 'Windows' && !vars.SELF_HOSTED
working-directory: ${{ github.workspace }}/build
@@ -419,6 +444,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'
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

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

@@ -20,6 +20,20 @@ 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})
# inspect_system_cache: dumps contents of a .cache file for debugging.
add_executable(inspect_system_cache inspect_system_cache.cpp)
target_link_libraries(inspect_system_cache libslic3r boost_headeronly)
target_compile_definitions(inspect_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,133 @@
#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);
// In validation_mode, load_system_presets_from_json uses data_dir() directly
// (no /system/ suffix), so point data_dir at the profiles directory.
set_data_dir(profiles_path);
set_resources_dir(fs::path(profiles_path).parent_path().make_preferred().string());
// load_presets creates user preset dirs under data_dir().
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;
}
// Collect all vendor names from JSON files in the profiles directory.
std::vector<std::string> vendor_names;
for (const auto& e : fs::directory_iterator(profiles_path)) {
if (e.path().extension() == ".json")
vendor_names.push_back(e.path().stem().string());
}
// Sort: PresetBundle::ORCA_FILAMENT_LIBRARY first, rest alphabetical.
std::sort(vendor_names.begin(), vendor_names.end(),
[](const std::string& a, const std::string& b) {
if (a == PresetBundle::ORCA_FILAMENT_LIBRARY) return true;
if (b == PresetBundle::ORCA_FILAMENT_LIBRARY) return false;
return a < b;
});
size_t total_print = 0, total_filament = 0, total_printer = 0;
int saved = 0, failed = 0;
for (const auto& vendor_name : vendor_names) {
try {
const std::string json_path = (fs::path(profiles_path) / (vendor_name + ".json")).string();
const std::string ver_str = get_vendor_cache_key(json_path);
const bool is_orca_lib = (vendor_name == PresetBundle::ORCA_FILAMENT_LIBRARY);
Slic3r::PresetBundle::VendorCache vc;
vc.capture(*preset_bundle, vendor_name, ver_str, is_orca_lib);
const std::string cache_path =
(fs::path(profiles_path) / (vendor_name + ".cache")).make_preferred().string();
vc.save(cache_path);
// Verify the file was written and can be reloaded.
Slic3r::PresetBundle::VendorCache verify;
if (!verify.load(cache_path) || !verify.is_valid(ver_str)) {
std::cerr << "ERROR: " << vendor_name << ": verification failed\n";
++failed;
} else {
std::cout << " [ok] " << vendor_name << ".cache"
<< " (" << vc.print_presets.size() << " print, "
<< vc.filament_presets.size() << " filament, "
<< vc.printer_presets.size() << " printer)\n";
total_print += vc.print_presets.size();
total_filament += vc.filament_presets.size();
total_printer += vc.printer_presets.size();
++saved;
}
} catch (const std::exception& ex) {
std::cerr << "ERROR: " << vendor_name << ": " << ex.what() << "\n";
++failed;
}
}
std::cout << "\nDone: " << saved << " cache(s) written";
if (failed) std::cout << ", " << failed << " FAILED";
std::cout << "\n"
<< " Total print presets: " << total_print << "\n"
<< " Total filament presets: " << total_filament << "\n"
<< " Total printer presets: " << total_printer << "\n";
return failed ? 1 : 0;
}

View File

@@ -0,0 +1,134 @@
#include "libslic3r/PresetBundle.hpp"
#include <boost/filesystem.hpp>
#include <boost/program_options.hpp>
#include <iostream>
#include <iomanip>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace po = boost::program_options;
static void print_bar(char c, int n) { std::cout << std::string(n, c) << "\n"; }
static void inspect_one(const std::string& path, const po::variables_map& vm)
{
Slic3r::PresetBundle::VendorCache vc;
if (!vc.load(path)) {
std::cerr << "Failed to load cache: " << path << "\n"
<< " (wrong format version, truncated file, CRC mismatch, or not a .cache file)\n";
return;
}
bool show_all = !vm.count("vendors") && !vm.count("models") &&
!vm.count("presets") && !vm.count("filaments") &&
!vm.count("printers") && !vm.count("process");
// ---- Summary ----
print_bar('=', 60);
std::cout << "Cache file : " << path << "\n";
std::cout << "Vendor : " << vc.profile.id << " v" << vc.profile.config_version << "\n";
std::cout << "JSON version: " << (vc.vendor_json_version.empty() ? "(none)" : vc.vendor_json_version) << "\n";
std::cout << "Cache ver : " << vc.cache_version << "\n";
std::cout << "Config opts : " << vc.config_options_count << "\n";
print_bar('-', 60);
std::cout << "Models : " << vc.profile.models.size() << "\n";
std::cout << "Printers : " << vc.printer_presets.size() << "\n";
std::cout << "Filaments : " << vc.filament_presets.size() << "\n";
std::cout << "Print proc : " << vc.print_presets.size() << "\n";
std::cout << "SLA print : " << vc.sla_print_presets.size() << "\n";
std::cout << "SLA material: " << vc.sla_material_presets.size() << "\n";
std::cout << "config_maps : " << vc.config_maps.size() << "\n";
std::cout << "filament_id_maps: " << vc.filament_id_maps.size() << "\n";
print_bar('=', 60);
// ---- Models ----
if (show_all || vm.count("vendors") || vm.count("models")) {
std::cout << "\nVENDOR PROFILE [" << vc.profile.id << "]\n";
print_bar('-', 60);
std::cout << " Name: " << vc.profile.name << "\n";
std::cout << " Config version: " << vc.profile.config_version << "\n";
if (vm.count("models") || show_all) {
for (const auto& m : vc.profile.models) {
std::cout << " " << std::left << std::setw(40) << m.name
<< " variants:" << m.variants.size() << "\n";
}
}
}
// ---- Printer presets ----
if (vm.count("presets") || vm.count("printers")) {
std::cout << "\nPRINTER PRESETS (" << vc.printer_presets.size() << ")\n";
print_bar('-', 60);
for (const auto& cp : vc.printer_presets) {
const auto* pm = cp.config.option<ConfigOptionString>("printer_model");
const auto* pv = cp.config.option<ConfigOptionString>("printer_variant");
std::cout << " " << std::left << std::setw(50) << cp.name
<< " model=" << (pm ? pm->value : "?")
<< " nozzle=" << (pv ? pv->value : "?")
<< (cp.is_visible ? "" : " [hidden]") << "\n";
}
}
// ---- Filament presets ----
if (vm.count("presets") || vm.count("filaments")) {
std::cout << "\nFILAMENT PRESETS (" << vc.filament_presets.size() << ")\n";
print_bar('-', 60);
for (const auto& cp : vc.filament_presets) {
const auto* fv = cp.config.option<ConfigOptionStrings>("filament_vendor");
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
std::cout << " " << std::left << std::setw(50) << cp.name
<< " vendor=" << (fv && !fv->values.empty() ? fv->values[0] : "?")
<< " type=" << (ft && !ft->values.empty() ? ft->values[0] : "?")
<< (cp.is_visible ? "" : " [hidden]") << "\n";
}
}
// ---- Print process presets ----
if (vm.count("presets") || vm.count("process")) {
std::cout << "\nPRINT PROCESS PRESETS (" << vc.print_presets.size() << ")\n";
print_bar('-', 60);
for (const auto& cp : vc.print_presets)
std::cout << " " << cp.name << (cp.is_visible ? "" : " [hidden]") << "\n";
}
}
int main(int argc, char* argv[])
{
po::options_description desc("OrcaSlicer Cache Inspector\nUsage");
desc.add_options()
("help,h", "Show help")
("path,p", po::value<std::string>(), "Path to a .cache file or a directory of .cache files (required)")
("vendors,V", "Show vendor profile summary")
("models,m", "List all printer models")
("presets,P", "List all preset names")
("filaments,f", "List filament presets")
("printers,r", "List printer presets")
("process,p2", "List print process presets");
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, desc), vm);
if (vm.count("help") || !vm.count("path")) { 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 path = vm["path"].as<std::string>();
if (fs::is_directory(path)) {
// Inspect all .cache files in the directory.
std::vector<fs::path> files;
for (const auto& e : fs::directory_iterator(path))
if (e.path().extension() == ".cache")
files.push_back(e.path());
std::sort(files.begin(), files.end());
for (const auto& f : files)
inspect_one(f.string(), vm);
} else {
inspect_one(path, vm);
}
return 0;
}

View File

@@ -145,6 +145,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
@@ -707,6 +721,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"
@@ -103,6 +111,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);
@@ -120,6 +132,9 @@ public:
PrinterVariant() {}
PrinterVariant(const std::string &name) : name(name) {}
std::string name;
template<class Archive>
void serialize(Archive& ar) { ar(name); }
};
struct PrinterModel {
@@ -150,6 +165,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;
@@ -161,6 +185,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);
@@ -394,12 +425,25 @@ 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 the fields needed to reconstruct a system preset from a binary cache.
// The vendor pointer is NOT serialized — apply() reconstructs it from VendorCache::profile.id.
template<class Archive>
void serialize(Archive& ar)
{
ar(type, name, alias, file, version,
filament_id, setting_id, description,
renamed_from, is_system, is_visible,
m_from_orca_filament_lib, 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);
@@ -539,6 +549,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;
@@ -947,6 +963,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)) {
@@ -965,7 +983,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));
@@ -1002,7 +1019,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));
@@ -1023,34 +1039,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();
@@ -2178,9 +2201,84 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
if (validation_mode)
dir = (boost::filesystem::path(data_dir())).make_preferred();
// Per-vendor binary cache: try user cache, then bundled cache, then JSON parse.
// Vendors that hit a cache are applied immediately; misses go through JSON parsing below.
std::set<std::string> cache_miss_vendors;
std::map<std::string, std::string> vendor_json_versions; // vendor_id → version string on disk
if (!validation_mode) {
const auto t0 = std::chrono::steady_clock::now();
this->reset(false);
bool all_from_cache = true;
// Collect all vendor names from the system directory.
std::vector<std::string> all_vendor_names;
try {
for (const auto& e : boost::filesystem::directory_iterator(dir)) {
if (Slic3r::is_json_file(e.path().string()))
all_vendor_names.push_back(e.path().stem().string());
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "PresetBundle: cannot scan system dir: " << ex.what();
}
// Load ORCA_FILAMENT_LIBRARY first (other vendors' filaments inherit from it).
// Then load remaining vendors from per-vendor caches.
auto try_vendor_cache = [&](const std::string& vendor_name) -> bool {
const std::string json_path = (dir / (vendor_name + ".json")).string();
const std::string ver_str = get_vendor_cache_key(json_path);
vendor_json_versions[vendor_name] = ver_str;
VendorCache vc;
// 1. User per-vendor cache
if (vc.load(VendorCache::user_path(vendor_name)) && vc.is_valid(ver_str)) {
vc.apply(*this);
return true;
}
// 2. Bundled per-vendor cache (ships with installer)
if (vc.load(VendorCache::bundled_path(vendor_name)) && vc.is_valid(ver_str)) {
vc.apply(*this);
vc.save(VendorCache::user_path(vendor_name)); // promote
return true;
}
return false;
};
// Sort: ORCA_FILAMENT_LIBRARY goes first, rest alphabetical.
std::sort(all_vendor_names.begin(), all_vendor_names.end(),
[](const std::string& a, const std::string& b) {
if (a == ORCA_FILAMENT_LIBRARY) return true;
if (b == ORCA_FILAMENT_LIBRARY) return false;
return a < b;
});
for (const auto& name : all_vendor_names) {
if (!try_vendor_cache(name)) {
cache_miss_vendors.insert(name);
all_from_cache = false;
}
}
if (all_from_cache && !all_vendor_names.empty()) {
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 per-vendor cache in " << ms << " ms";
return {PresetsConfigSubstitutions{}, ""};
}
if (!cache_miss_vendors.empty())
BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << cache_miss_vendors.size()
<< " vendor(s) need JSON parse: "
<< [&]{ std::string s; for (auto& v : cache_miss_vendors) s += v + " "; return s; }();
}
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
// first = true means no vendor has been loaded yet (from cache or JSON).
// false = at least one vendor was applied from the per-vendor cache above.
bool first = validation_mode || cache_miss_vendors.size() == vendor_json_versions.size();
std::vector<std::string> vendor_names;
// store all vendor names in vendor_names
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
@@ -2202,6 +2300,9 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
std::vector<std::string> other_vendors;
other_vendors.reserve(vendor_names.size());
for (auto& vn : vendor_names) {
// Skip vendors already loaded from the per-vendor cache.
if (!validation_mode && !cache_miss_vendors.count(vn))
continue;
if (vn == ORCA_FILAMENT_LIBRARY)
orca_lib_vendor = vn;
else if (!(validation_mode && !vendor_to_validate.empty() && vn != vendor_to_validate))
@@ -2218,6 +2319,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);
}
}
@@ -2254,6 +2356,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])
@@ -2281,6 +2384,33 @@ 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 per-vendor binary caches for vendors that parsed successfully.
// Vendors whose JSON produced a parse error are skipped individually so one
// bad vendor does not block caching of all others.
if (!validation_mode && !cache_miss_vendors.empty()) {
const auto save_t0 = std::chrono::steady_clock::now();
for (const auto& vendor_name : cache_miss_vendors) {
if (errored_vendors.count(vendor_name))
continue;
const bool is_orca_lib = (vendor_name == ORCA_FILAMENT_LIBRARY);
const std::string ver_str = vendor_json_versions.count(vendor_name)
? vendor_json_versions.at(vendor_name) : "";
VendorCache vc;
vc.capture(*this, vendor_name, ver_str, is_orca_lib);
vc.save(VendorCache::user_path(vendor_name));
}
const auto save_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - save_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: per-vendor caches saved in " << save_ms << " ms";
}
//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);
@@ -5558,4 +5688,205 @@ bool BundleMetadata::save_to_json(const std::string& path) const
return false;
}
}
// ---- VendorCache 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");
// Returns the string used as vendor_json_version in the cache validity check.
// For versioned vendors this is the Semver string from the JSON.
// For version-less vendors we use the file mtime so any edit invalidates the cache.
static 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();
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));
}
template<class T>
static void save_blob(const std::string& path, const T& obj)
{
std::ostringstream oss;
{
cereal::BinaryOutputArchive ar(oss);
ar(obj);
}
const std::string blob = oss.str();
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) << "VendorCache: cannot open for writing: " << path;
return;
}
CacheFileHeader hdr;
hdr.magic = PresetBundle::VendorCache::CACHE_MAGIC;
hdr.version = PresetBundle::VendorCache::CACHE_VERSION;
hdr.data_size = static_cast<uint64_t>(blob.size());
hdr.crc32 = crc.checksum();
ofs.write(reinterpret_cast<const char*>(&hdr), sizeof(hdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: write failed (" << path << "): " << e.what();
}
}
template<class T>
static bool load_blob(const std::string& path, T& obj)
{
try {
boost::nowide::ifstream ifs(path, std::ios::binary);
if (!ifs.is_open())
return false;
CacheFileHeader hdr;
if (!ifs.read(reinterpret_cast<char*>(&hdr), sizeof(hdr)))
return false;
if (hdr.magic != PresetBundle::VendorCache::CACHE_MAGIC || hdr.version != PresetBundle::VendorCache::CACHE_VERSION)
return false;
if (hdr.data_size == 0 || hdr.data_size > 512u * 1024u * 1024u)
return false;
std::string blob(hdr.data_size, '\0');
if (!ifs.read(&blob[0], static_cast<std::streamsize>(hdr.data_size)))
return false;
boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size());
if (crc.checksum() != hdr.crc32) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: CRC mismatch: " << path;
return false;
}
std::istringstream iss(blob);
cereal::BinaryInputArchive ar(iss);
ar(obj);
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCache: load failed (" << path << "): " << e.what();
return false;
}
}
} // anonymous namespace
std::string PresetBundle::VendorCache::user_path(const std::string& vendor_id)
{
return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / (vendor_id + ".cache"))
.make_preferred().string();
}
std::string PresetBundle::VendorCache::bundled_path(const std::string& vendor_id)
{
return (boost::filesystem::path(resources_dir()) / "profiles" / (vendor_id + ".cache"))
.make_preferred().string();
}
bool PresetBundle::VendorCache::load(const std::string& path)
{
return load_blob(path, *this);
}
void PresetBundle::VendorCache::save(const std::string& path) const
{
save_blob(path, *this);
}
void PresetBundle::VendorCache::capture(const PresetBundle& bundle,
const std::string& vendor_id,
const std::string& vendor_json_ver,
bool capture_filament_maps)
{
cache_version = CACHE_VERSION;
config_options_count = static_cast<uint32_t>(print_config_def.options.size());
vendor_json_version = vendor_json_ver;
print_presets.clear();
filament_presets.clear();
printer_presets.clear();
sla_print_presets.clear();
sla_material_presets.clear();
config_maps.clear();
filament_id_maps.clear();
// Vendor profile — copy directly; VendorProfile now carries its own serialize()
auto vp_it = bundle.vendors.find(vendor_id);
if (vp_it != bundle.vendors.end())
profile = vp_it->second;
// Presets — only those belonging to this vendor; Preset now carries its own serialize()
auto capture_col = [&](const PresetCollection& coll, std::vector<Preset>& out) {
for (const Preset& p : coll()) {
if (!p.is_system) continue;
if (p.vendor == nullptr || p.vendor->id != vendor_id) continue;
out.push_back(p); // vendor pointer not serialized; apply() reconstructs it
}
};
capture_col(bundle.prints, print_presets);
capture_col(bundle.filaments, filament_presets);
capture_col(bundle.printers, printer_presets);
capture_col(bundle.sla_prints, sla_print_presets);
capture_col(bundle.sla_materials, sla_material_presets);
if (capture_filament_maps) {
config_maps = bundle.m_config_maps;
filament_id_maps = bundle.m_filament_id_maps;
}
}
void PresetBundle::VendorCache::apply(PresetBundle& bundle) const
{
// Restore vendor profile (additive — does not reset the bundle)
bundle.vendors.emplace(profile.id, profile);
// Restore presets; vendor pointer is reconstructed from profile.id since all
// presets in this cache belong to the same vendor.
const VendorProfile* vp = nullptr;
{
auto it = bundle.vendors.find(profile.id);
if (it != bundle.vendors.end())
vp = &it->second;
}
auto apply_col = [&](const std::vector<Preset>& cached,
PresetCollection& coll,
bool is_filaments) {
for (const Preset& cp : cached) {
DynamicPrintConfig config = cp.config;
Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), /*select=*/false, cp.version);
p.is_system = true;
p.is_visible = cp.is_visible;
p.alias = cp.alias;
p.renamed_from = cp.renamed_from;
p.filament_id = cp.filament_id;
p.setting_id = cp.setting_id;
p.description = cp.description;
p.m_from_orca_filament_lib = cp.m_from_orca_filament_lib;
p.vendor = vp;
if (is_filaments)
coll.set_printer_hold_alias(p.alias, p);
}
};
apply_col(print_presets, bundle.prints, false);
apply_col(filament_presets, bundle.filaments, true);
apply_col(printer_presets, bundle.printers, false);
apply_col(sla_print_presets, bundle.sla_prints, false);
apply_col(sla_material_presets, bundle.sla_materials, false);
if (!config_maps.empty()) {
bundle.m_config_maps = config_maps;
bundle.m_filament_id_maps = filament_id_maps;
}
}
} // 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,64 @@ struct PresetBundleMetadata
class PresetBundle
{
public:
// ---- Per-vendor binary preset cache ------------------------------------
// One file per vendor:
// Bundled (CI-generated): resources/profiles/<vendor_id>.cache
// User (runtime): data_dir/system/<vendor_id>.cache
// VendorProfile, VendorProfile::PrinterModel, and Preset carry their own
// cereal serialize() methods so no mirror structs are needed.
struct VendorCache {
static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
static constexpr uint32_t CACHE_VERSION = 5; // bumped: Semver serialized as 3-part (to_string_sf)
uint32_t cache_version = CACHE_VERSION;
uint32_t config_options_count = 0; // fixed-width: size_t varies between 32/64-bit builds
std::string vendor_json_version; // Semver string, or mtime:<timestamp> for version-less vendors
VendorProfile profile;
std::vector<Preset> print_presets;
std::vector<Preset> filament_presets;
std::vector<Preset> printer_presets;
std::vector<Preset> sla_print_presets;
std::vector<Preset> sla_material_presets;
// Only populated for the ORCA_FILAMENT_LIBRARY vendor.
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, vendor_json_version,
profile,
print_presets, filament_presets, printer_presets,
sla_print_presets, sla_material_presets,
config_maps, filament_id_maps);
}
bool is_valid(const std::string& current_vendor_json_version) const
{
return cache_version == CACHE_VERSION
&& config_options_count == static_cast<uint32_t>(print_config_def.options.size())
&& vendor_json_version == current_vendor_json_version;
}
static std::string user_path(const std::string& vendor_id); // data_dir/system/<id>.cache
static std::string bundled_path(const std::string& vendor_id); // resources/profiles/<id>.cache
bool load(const std::string& path);
void save(const std::string& path) const;
void capture(const PresetBundle& bundle,
const std::string& vendor_id,
const std::string& vendor_json_version,
bool capture_filament_maps);
void apply(PresetBundle& bundle) const;
};
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
Preset &in_print_preset,
const DynamicPrintConfig &project_config,

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

@@ -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,8 +43,6 @@ using namespace nlohmann;
namespace Slic3r { namespace GUI {
json m_ProfileJson;
static wxString update_custom_filaments()
{
json m_Res = json::object();
@@ -191,11 +191,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 +300,65 @@ 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()) {
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();
@@ -1133,99 +1182,321 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
return 0;
}
int GuideFrame::LoadProfileData()
bool GuideFrame::BuildProfileDataFromPresetBundle()
{
PresetBundle* pb = wxGetApp().preset_bundle;
if (!pb || pb->vendors.empty())
return false;
try {
m_ProfileJson = json::parse("{}");
// 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/<vendor>.cache, generated by CI).
// This avoids the 90-second LoadProfileFamily fallback on first launch.
bool GuideFrame::BuildProfileDataFromBundledCache()
{
// Enumerate per-vendor .cache files in resources/profiles/.
std::vector<boost::filesystem::path> cache_files;
try {
for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) {
if (e.path().extension() == ".cache")
cache_files.push_back(e.path());
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache: cannot scan " << rsrc_vendor_dir << ": " << ex.what();
return false;
}
if (cache_files.empty())
return false;
// Orca: add custom as default
// Orca: add json logic for vendor bundle
orca_bundle_rsrc = true;
try {
for (const auto& cache_path : cache_files) {
const std::string vendor_id = cache_path.stem().string();
// Validate against the version in the corresponding vendor JSON.
const boost::filesystem::path json_path = rsrc_vendor_dir / (vendor_id + ".json");
const std::string ver_str = get_vendor_cache_key(json_path.string());
// 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;
PresetBundle::VendorCache vc;
if (!vc.load(cache_path.string()) || !vc.is_valid(ver_str))
continue;
// Models from this vendor's cached profile
for (const auto& cm : vc.profile.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" / vc.profile.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"] = vc.profile.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);
}
// Machines from cached printer presets
for (const auto& cp : vc.printer_presets) {
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;
json mach;
mach["model"] = pm->value;
mach["nozzle"] = pv->value;
m_ProfileJson["machine"][cp.name] = mach;
}
// Filaments from cached filament presets
for (const auto& cp : vc.filament_presets) {
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");
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 + "]";
}
}
}
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;
}
// Process from cached print presets
for (const auto& cp : vc.print_presets) {
if (!cp.is_visible) continue;
json entry;
entry["name"] = cp.name;
entry["sub_path"] = cp.file;
m_ProfileJson["process"].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);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from bundled per-vendor caches ("
<< 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;
}
}
//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('/');
int GuideFrame::LoadProfileData()
{
// Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded).
// Try bundled per-vendor caches first, then fall back to reading all vendor JSONs.
try {
// Bundled system preset cache (CI-generated, ~1-2s)
bool slow_path = !BuildProfileDataFromBundledCache();
if (slow_path) {
// 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);
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));
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;
}
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;
}
wxGetApp().CallAfter([this] {
if (!m_destroy) {
//sync to appconfig first to populate current selections
SaveProfileData();
//sync to web after selections are populated
std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
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));
RunScript(strJS);
}
// 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,10 @@ 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();
int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
@@ -112,8 +120,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 +134,7 @@ private:
bool InstallNetplugin;
bool network_plugin_ready {false};
json m_ProfileJson;
json m_OrcaFilaList;
std::string m_OrcaFilaLibPath;