Compare commits

...

9 Commits

12 changed files with 1286 additions and 68 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,15 @@ 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
$profiles = Get-ChildItem -Recurse -Path build -Directory -Filter profiles |
Where-Object { $_.FullName -match 'resources' } | Select-Object -First 1
& $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 +437,21 @@ 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)
"$tool" --path build/package/resources/profiles --log_level 2
# Re-pack the AppImage so the cache is included
appimage=$(find build -maxdepth 1 -name "OrcaSlicer_Linux_AppImage*.AppImage" | head -1)
chmod +x "$appimage"
"$appimage" --appimage-extract
cp build/package/resources/profiles/system_presets_cache.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

@@ -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 resources/profiles/system_presets_cache.cache 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,86 @@
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PresetBundleCache.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/program_options.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;
}
const std::string cache_path =
(fs::path(profiles_path) / "system_presets_cache.cache").make_preferred().string();
PresetBundleCache::SystemPresetsCache cache;
cache.capture(*preset_bundle, profiles_path);
cache.save(cache_path);
std::cout << "Cache written: " << cache_path << "\n"
<< " Vendor profiles: " << cache.vendor_profiles.size() << "\n"
<< " Print presets: " << cache.print_presets.size() << "\n"
<< " Filament presets: " << cache.filament_presets.size() << "\n"
<< " Printer presets: " << cache.printer_presets.size() << "\n";
return 0;
}

View File

@@ -0,0 +1,126 @@
#include "libslic3r/PresetBundleCache.hpp"
#include <boost/program_options.hpp>
#include <iostream>
#include <iomanip>
using namespace Slic3r;
namespace po = boost::program_options;
static void print_bar(char c, int n) { std::cout << std::string(n, c) << "\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 .cache file (required)")
("vendors,V", "List all vendor IDs and versions")
("models,m", "List all printer models per vendor")
("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>();
PresetBundleCache::SystemPresetsCache cache;
if (!cache.load(path)) {
std::cerr << "Failed to load cache: " << path << "\n"
<< " (wrong format version, truncated file, or not a .cache file)\n";
return 1;
}
// ---- Summary ----
print_bar('=', 60);
std::cout << "Cache file : " << path << "\n";
std::cout << "Format ver : " << cache.format_version << "\n";
std::cout << "Config opts: " << cache.config_options_count << "\n";
print_bar('-', 60);
std::cout << "Vendors : " << cache.vendor_versions.size() << "\n";
std::cout << "Models : ";
size_t total_models = 0;
for (const auto& vp : cache.vendor_profiles) total_models += vp.models.size();
std::cout << total_models << "\n";
std::cout << "Printers : " << cache.printer_presets.size() << "\n";
std::cout << "Filaments : " << cache.filament_presets.size() << "\n";
std::cout << "Print proc : " << cache.print_presets.size() << "\n";
std::cout << "config_maps: " << cache.config_maps.size() << "\n";
std::cout << "filament_id_maps: " << cache.filament_id_maps.size() << "\n";
print_bar('=', 60);
bool show_all = !vm.count("vendors") && !vm.count("models") &&
!vm.count("presets") && !vm.count("filaments") &&
!vm.count("printers") && !vm.count("process");
// ---- Vendor versions ----
if (show_all || vm.count("vendors")) {
std::cout << "\nVENDOR VERSIONS (" << cache.vendor_versions.size() << ")\n";
print_bar('-', 60);
for (const auto& [id, ver] : cache.vendor_versions)
std::cout << " " << std::left << std::setw(30) << id << " " << ver << "\n";
}
// ---- Models per vendor ----
if (show_all || vm.count("models")) {
std::cout << "\nVENDOR PROFILES & MODELS\n";
print_bar('-', 60);
for (const auto& vp : cache.vendor_profiles) {
std::cout << " [" << vp.id << "] v" << vp.config_version
<< " (" << vp.models.size() << " models)\n";
if (vm.count("models")) {
for (const auto& m : vp.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 (" << cache.printer_presets.size() << ")\n";
print_bar('-', 60);
for (const auto& cp : cache.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 (" << cache.filament_presets.size() << ")\n";
print_bar('-', 60);
for (const auto& cp : cache.filament_presets) {
const auto* fv = cp.config.option<ConfigOptionString>("filament_vendor");
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
std::cout << " " << std::left << std::setw(50) << cp.name
<< " vendor=" << (fv ? fv->value : "?")
<< " 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 (" << cache.print_presets.size() << ")\n";
print_bar('-', 60);
for (const auto& cp : cache.print_presets)
std::cout << " " << cp.name << (cp.is_visible ? "" : " [hidden]") << "\n";
}
return 0;
}

View File

@@ -344,6 +344,8 @@ set(lisbslic3r_sources
Polyline.hpp
PresetBundle.cpp
PresetBundle.hpp
PresetBundleCache.cpp
PresetBundleCache.hpp
Preset.cpp
Preset.hpp
PrincipalComponents2D.cpp

View File

@@ -145,6 +145,9 @@ 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();
}
}
//BBS: add a function to load the key-values from xxx.json
@@ -707,6 +710,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

@@ -1,7 +1,9 @@
#include <cassert>
#include <chrono>
#include <ctime>
#include "PresetBundle.hpp"
#include "PresetBundleCache.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include "I18N.hpp"
@@ -520,6 +522,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 +543,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 +957,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 +977,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 +1013,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 +1033,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 +2195,60 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
if (validation_mode)
dir = (boost::filesystem::path(data_dir())).make_preferred();
// Try loading from binary cache first (skips JSON parsing on cache hit).
// partial_dirty_vendors is non-empty when some vendors changed but others are still cached.
std::set<std::string> partial_dirty_vendors;
if (!validation_mode) {
const auto t0 = std::chrono::steady_clock::now();
PresetBundleCache::SystemPresetsCache cache;
const std::string cache_file = PresetBundleCache::SystemPresetsCache::cache_path();
if (cache.load(cache_file) && cache.is_plausible()) {
std::set<std::string> dirty;
if (cache.get_dirty_vendors(dir.string(), dirty)) {
if (dirty.empty()) {
// Full hit — every vendor is unchanged.
cache.apply(*this);
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 cache in " << ms << " ms";
return {PresetsConfigSubstitutions{}, ""};
}
// Partial hit — restore clean vendors from cache, re-parse only dirty ones.
BOOST_LOG_TRIVIAL(info) << "PresetBundle: partial cache hit, " << dirty.size()
<< " vendor(s) changed — re-parsing those only";
cache.apply_partial(*this, dirty);
partial_dirty_vendors = std::move(dirty);
}
// else: structural mismatch (format version or option count changed) → full re-parse.
}
if (partial_dirty_vendors.empty()) {
// No partial hit — try bundled cache shipped with the installer (first launch).
{
const std::string bundled_dir =
(boost::filesystem::path(resources_dir()) / "profiles").make_preferred().string();
PresetBundleCache::SystemPresetsCache bundled;
if (bundled.load(PresetBundleCache::SystemPresetsCache::bundled_cache_path()) &&
bundled.is_valid(bundled_dir) && bundled.is_plausible()) {
bundled.apply(*this);
update_system_maps();
// Promote to user cache so subsequent launches skip this check.
bundled.save(cache_file);
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 bundled cache in " << ms << " ms";
return {PresetsConfigSubstitutions{}, ""};
}
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cache miss, falling back to JSON load";
}
}
const auto json_load_t0 = std::chrono::steady_clock::now();
PresetsConfigSubstitutions substitutions;
std::string errors_cummulative;
bool first = true;
bool first = partial_dirty_vendors.empty(); // false in partial mode: clean vendors already applied
std::vector<std::string> vendor_names;
// store all vendor names in vendor_names
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
@@ -2202,6 +2270,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) {
// In partial mode, skip vendors already loaded from cache.
if (!partial_dirty_vendors.empty() && !partial_dirty_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))
@@ -2281,6 +2352,24 @@ 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";
}
// Persist a binary cache so the next startup can skip JSON parsing.
if (!validation_mode && errors_cummulative.empty()) {
const auto save_t0 = std::chrono::steady_clock::now();
PresetBundleCache::SystemPresetsCache cache;
cache.capture(*this, dir.string());
cache.save(PresetBundleCache::SystemPresetsCache::cache_path());
const auto save_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - save_t0).count();
BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets cache 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);

View File

@@ -0,0 +1,346 @@
#include "PresetBundleCache.hpp"
#include <sstream>
#include <boost/crc.hpp>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <cereal/archives/binary.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include "PresetBundle.hpp"
#include "PrintConfig.hpp"
#include "Semver.hpp"
#include "Utils.hpp"
namespace Slic3r {
namespace PresetBundleCache {
// -------------------------------------------------------------------------
// Binary cache file format: raw 20-byte header followed by cereal blob.
// -------------------------------------------------------------------------
static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
static constexpr uint32_t CACHE_FILE_VERSION = 1;
#pragma pack(push, 1)
struct CacheFileHeader {
uint32_t magic;
uint32_t file_version;
uint64_t data_size;
uint32_t crc32;
};
#pragma pack(pop)
static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes");
template<class T>
static void save_blob(const std::string& path, const T& obj)
{
std::ostringstream oss(std::ios::out | std::ios::binary);
{
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) << "PresetBundleCache: cannot open for writing: " << path;
return;
}
CacheFileHeader hdr;
hdr.magic = CACHE_MAGIC;
hdr.file_version = CACHE_FILE_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) << "PresetBundleCache: 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 != CACHE_MAGIC || hdr.file_version != CACHE_FILE_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) << "PresetBundleCache: CRC32 mismatch: " << path;
return false;
}
std::istringstream iss(blob, std::ios::in | std::ios::binary);
cereal::BinaryInputArchive ar(iss);
ar(obj);
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: load failed (" << path << "): " << e.what();
return false;
}
}
// -------------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------------
static std::string vendor_root_json(const std::string& system_dir, const std::string& vendor_id)
{
return (boost::filesystem::path(system_dir) / (vendor_id + ".json")).make_preferred().string();
}
// -------------------------------------------------------------------------
// SystemPresetsCache
// -------------------------------------------------------------------------
std::string SystemPresetsCache::cache_path()
{
return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / "system_presets_cache.cache")
.make_preferred().string();
}
std::string SystemPresetsCache::bundled_cache_path()
{
return (boost::filesystem::path(resources_dir()) / "profiles" / "system_presets_cache.cache")
.make_preferred().string();
}
bool SystemPresetsCache::is_valid(const std::string& system_dir) const
{
std::set<std::string> dummy;
return get_dirty_vendors(system_dir, dummy) && dummy.empty();
}
bool SystemPresetsCache::get_dirty_vendors(const std::string& system_dir, std::set<std::string>& out_dirty) const
{
out_dirty.clear();
if (format_version != FORMAT_VERSION || config_options_count != print_config_def.options.size())
return false;
std::map<std::string, std::string> current;
try {
for (const auto& entry : boost::filesystem::directory_iterator(system_dir)) {
const std::string path = entry.path().string();
if (!Slic3r::is_json_file(path))
continue;
const std::string vendor_name = entry.path().stem().string();
Semver ver = get_version_from_json(path);
if (ver.valid())
current[vendor_name] = ver.to_string();
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: directory scan failed: " << e.what();
return false;
}
// A vendor was removed from disk — safest to force a full re-parse.
for (const auto& [name, ver] : vendor_versions)
if (current.find(name) == current.end())
return false;
// Collect vendors with changed or new version strings.
for (const auto& [name, ver] : current) {
auto it = vendor_versions.find(name);
if (it == vendor_versions.end() || it->second != ver)
out_dirty.insert(name);
}
return true;
}
void SystemPresetsCache::capture(const PresetBundle& bundle, const std::string& system_dir)
{
format_version = FORMAT_VERSION;
config_options_count = print_config_def.options.size();
vendor_versions.clear();
vendor_profiles.clear();
print_presets.clear();
filament_presets.clear();
printer_presets.clear();
sla_print_presets.clear();
sla_material_presets.clear();
config_maps = bundle.m_config_maps;
filament_id_maps = bundle.m_filament_id_maps;
for (const auto& [id, vp] : bundle.vendors) {
CachedVendorProfile cvp;
cvp.id = vp.id;
cvp.name = vp.name;
cvp.config_version = vp.config_version.valid() ? vp.config_version.to_string() : "";
cvp.config_update_url = vp.config_update_url;
cvp.changelog_url = vp.changelog_url;
for (const auto& model : vp.models) {
CachedPrinterModel cm;
cm.id = model.id;
cm.name = model.name;
cm.model_id = model.model_id;
cm.family = model.family;
cm.technology = static_cast<int>(model.technology);
for (const auto& v : model.variants)
cm.variants.push_back({v.name});
cm.default_materials = model.default_materials;
cm.not_support_bed_types = model.not_support_bed_types;
cm.bed_model = model.bed_model;
cm.bed_texture = model.bed_texture;
cm.image_bed_type = model.image_bed_type;
cm.bottom_texture_end_name = model.bottom_texture_end_name;
cm.use_double_extruder_default_texture = model.use_double_extruder_default_texture;
cm.bottom_texture_rect = model.bottom_texture_rect;
cm.middle_texture_rect = model.middle_texture_rect;
cm.hotend_model = model.hotend_model;
cvp.models.push_back(std::move(cm));
}
for (const auto& f : vp.default_filaments)
cvp.default_filaments.push_back(f);
for (const auto& m : vp.default_sla_materials)
cvp.default_sla_materials.push_back(m);
vendor_profiles.push_back(std::move(cvp));
Semver ver = get_version_from_json(vendor_root_json(system_dir, id));
vendor_versions[id] = ver.valid() ? ver.to_string() : "";
}
auto capture_col = [](const PresetCollection& coll, std::vector<CachedPreset>& out) {
for (const Preset& p : coll()) {
if (!p.is_system)
continue;
CachedPreset cp;
cp.type = static_cast<int>(p.type);
cp.name = p.name;
cp.alias = p.alias;
cp.file = p.file;
cp.version = p.version.valid() ? p.version.to_string() : "";
cp.vendor_id = (p.vendor != nullptr) ? p.vendor->id : "";
cp.filament_id = p.filament_id;
cp.setting_id = p.setting_id;
cp.description = p.description;
cp.renamed_from = p.renamed_from;
cp.is_system = p.is_system;
cp.is_visible = p.is_visible;
cp.m_from_orca_filament_lib = p.m_from_orca_filament_lib;
cp.config = p.config;
out.push_back(std::move(cp));
}
};
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);
}
void SystemPresetsCache::apply(PresetBundle& bundle) const
{
apply_partial(bundle, {});
}
void SystemPresetsCache::apply_partial(PresetBundle& bundle, const std::set<std::string>& skip_vendor_ids) const
{
bundle.reset(false);
for (const auto& cvp : vendor_profiles) {
if (skip_vendor_ids.count(cvp.id))
continue;
VendorProfile vp(cvp.id);
vp.name = cvp.name;
vp.config_update_url = cvp.config_update_url;
vp.changelog_url = cvp.changelog_url;
if (!cvp.config_version.empty()) {
auto v = Semver::parse(cvp.config_version);
if (v) vp.config_version = *v;
}
for (const auto& cm : cvp.models) {
VendorProfile::PrinterModel model;
model.id = cm.id;
model.name = cm.name;
model.model_id = cm.model_id;
model.family = cm.family;
model.technology = static_cast<PrinterTechnology>(cm.technology);
for (const auto& v : cm.variants)
model.variants.emplace_back(v.name);
model.default_materials = cm.default_materials;
model.not_support_bed_types = cm.not_support_bed_types;
model.bed_model = cm.bed_model;
model.bed_texture = cm.bed_texture;
model.image_bed_type = cm.image_bed_type;
model.bottom_texture_end_name = cm.bottom_texture_end_name;
model.use_double_extruder_default_texture = cm.use_double_extruder_default_texture;
model.bottom_texture_rect = cm.bottom_texture_rect;
model.middle_texture_rect = cm.middle_texture_rect;
model.hotend_model = cm.hotend_model;
vp.models.push_back(std::move(model));
}
for (const auto& f : cvp.default_filaments)
vp.default_filaments.insert(f);
for (const auto& m : cvp.default_sla_materials)
vp.default_sla_materials.insert(m);
bundle.vendors.emplace(cvp.id, std::move(vp));
}
auto apply_col = [&](const std::vector<CachedPreset>& cached,
PresetCollection& coll,
bool is_filaments) {
for (const auto& cp : cached) {
if (skip_vendor_ids.count(cp.vendor_id))
continue;
Semver version;
if (!cp.version.empty()) {
auto v = Semver::parse(cp.version);
if (v) version = *v;
}
DynamicPrintConfig config = cp.config;
Preset& p = coll.load_preset(cp.file, cp.name, std::move(config), /*select=*/false, 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;
if (!cp.vendor_id.empty()) {
auto it = bundle.vendors.find(cp.vendor_id);
if (it != bundle.vendors.end())
p.vendor = &it->second;
}
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);
// config_maps and filament_id_maps are derived from ORCA_FILAMENT_LIBRARY.
// If that vendor is in skip_vendor_ids it will be re-loaded from JSON and will
// overwrite these; otherwise the cached values are still valid.
bundle.m_config_maps = config_maps;
bundle.m_filament_id_maps = filament_id_maps;
// Caller must invoke bundle.update_system_maps() after all loading is done.
}
bool SystemPresetsCache::load(const std::string& path)
{
return load_blob(path, *this);
}
void SystemPresetsCache::save(const std::string& path) const
{
save_blob(path, *this);
}
} // namespace PresetBundleCache
} // namespace Slic3r

View File

@@ -0,0 +1,145 @@
#pragma once
#include <cstdint>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <cereal/archives/binary.hpp>
#include <cereal/cereal.hpp>
#include <cereal/types/map.hpp>
#include <cereal/types/polymorphic.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include "PrintConfig.hpp"
namespace Slic3r {
class PresetBundle;
namespace PresetBundleCache {
// ---- Vendor profile structures ----
struct CachedPrinterVariant {
std::string name;
template<class Archive> void serialize(Archive& ar) { ar(name); }
};
struct CachedPrinterModel {
std::string id, name, model_id, family;
int technology = 0;
std::vector<CachedPrinterVariant> variants;
std::vector<std::string> default_materials;
std::vector<std::string> not_support_bed_types;
std::string bed_model, bed_texture, image_bed_type;
std::string bottom_texture_end_name, use_double_extruder_default_texture;
std::string bottom_texture_rect, middle_texture_rect, hotend_model;
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);
}
};
struct CachedVendorProfile {
std::string id, name, config_version, config_update_url, changelog_url;
std::vector<CachedPrinterModel> models;
std::vector<std::string> default_filaments;
std::vector<std::string> default_sla_materials;
template<class Archive>
void serialize(Archive& ar)
{
ar(id, name, config_version, config_update_url, changelog_url,
models, default_filaments, default_sla_materials);
}
};
// ---- Per-preset cache entry ----
struct CachedPreset {
int type = 0;
std::string name, alias, file, version;
std::string vendor_id;
std::string filament_id, setting_id, description;
std::string base_id, user_id, sync_info;
long long updated_time = 0;
std::vector<std::string> renamed_from;
bool is_system = true;
bool is_visible = true;
bool m_from_orca_filament_lib = false;
DynamicPrintConfig config;
template<class Archive>
void serialize(Archive& ar)
{
ar(type, name, alias, file, version, vendor_id, filament_id, setting_id,
description, base_id, user_id, sync_info, updated_time, renamed_from,
is_system, is_visible, m_from_orca_filament_lib, config);
}
};
// ---- System preset cache ----
// Single blob at <data_dir>/system/system_presets_cache.cache
// Covers all vendor profiles and system presets.
// Invalidated when any vendor version string changes or config option count changes.
struct SystemPresetsCache {
static constexpr uint32_t FORMAT_VERSION = 2;
uint32_t format_version = FORMAT_VERSION;
size_t config_options_count = 0;
std::map<std::string, std::string> vendor_versions;
std::vector<CachedVendorProfile> vendor_profiles;
std::vector<CachedPreset> print_presets;
std::vector<CachedPreset> filament_presets;
std::vector<CachedPreset> printer_presets;
std::vector<CachedPreset> sla_print_presets;
std::vector<CachedPreset> sla_material_presets;
std::map<std::string, DynamicPrintConfig> config_maps;
std::map<std::string, std::string> filament_id_maps;
template<class Archive>
void serialize(Archive& ar)
{
ar(format_version, config_options_count, vendor_versions,
vendor_profiles,
print_presets, filament_presets, printer_presets,
sla_print_presets, sla_material_presets,
config_maps, filament_id_maps);
}
static std::string cache_path();
static std::string bundled_cache_path();
bool is_valid(const std::string& system_dir) const;
// Rejects caches that loaded structurally but contain no data or are internally inconsistent
// (e.g. vendor_versions lists all vendors but vendor_profiles only captured some of them).
bool is_plausible() const {
return !vendor_profiles.empty() &&
!printer_presets.empty() &&
vendor_versions.size() == vendor_profiles.size();
}
// Returns false on structural mismatch (full re-parse required).
// On true, populates out_dirty with vendor IDs whose version strings changed.
// Empty out_dirty means fully valid (cache hit, no re-parse needed).
bool get_dirty_vendors(const std::string& system_dir, std::set<std::string>& out_dirty) const;
void capture(const PresetBundle& bundle, const std::string& system_dir);
void apply(PresetBundle& bundle) const;
// Same as apply() but skips vendors listed in skip_vendor_ids and their presets.
void apply_partial(PresetBundle& bundle, const std::set<std::string>& skip_vendor_ids) const;
bool load(const std::string& path);
void save(const std::string& path) const;
};
} // namespace PresetBundleCache
} // namespace Slic3r

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>
@@ -10,6 +11,7 @@
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PresetBundleCache.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "libslic3r_version.h"
@@ -1133,6 +1135,355 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
return 0;
}
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;
}
}
static std::string guide_cache_path()
{
// Keep this out of data_dir/system/ — that directory is scanned for vendor JSONs
// and a non-vendor JSON there crashes get_version_from_json() on startup.
return (boost::filesystem::path(Slic3r::data_dir()) / "guide_profile_cache.json")
.make_preferred().string();
}
// Reads the "version" field from a vendor root JSON without parsing the full file.
// The version field is always within the first few hundred bytes, so we read
// only a small prefix instead of loading (potentially) megabyte-sized vendor files.
static std::string read_vendor_json_version(const boost::filesystem::path& path)
{
try {
boost::nowide::ifstream f(path.string());
if (!f.is_open()) return {};
char buf[512];
f.read(buf, sizeof(buf));
const std::string head(buf, static_cast<size_t>(f.gcount()));
const size_t kpos = head.find("\"version\"");
if (kpos == std::string::npos) return {};
const size_t colon = head.find(':', kpos);
if (colon == std::string::npos) return {};
const size_t q1 = head.find('"', colon + 1);
if (q1 == std::string::npos) return {};
const size_t q2 = head.find('"', q1 + 1);
if (q2 == std::string::npos) return {};
return head.substr(q1 + 1, q2 - q1 - 1);
} catch (...) {}
return {};
}
// Tries to load the user-side guide profile JSON cache.
// Returns true and populates m_ProfileJson on cache hit.
bool GuideFrame::try_load_guide_cache()
{
const std::string path = guide_cache_path();
if (!boost::filesystem::exists(path))
return false;
try {
std::string contents;
if (!LoadFile(path, contents))
return false;
const json cache = json::parse(contents);
if (!cache.contains("vendor_versions") || !cache.contains("profile_data"))
return false;
const json& vv = cache["vendor_versions"];
// Every vendor in the cache must still have the same version.
for (const auto& [filename, cached_ver] : vv.items()) {
boost::filesystem::path fp = rsrc_vendor_dir / filename;
if (!boost::filesystem::exists(fp))
fp = vendor_dir / filename;
if (!boost::filesystem::exists(fp))
return false;
if (read_vendor_json_version(fp) != cached_ver.get<std::string>())
return false;
}
// No new vendor JSONs must have appeared since the cache was built.
for (const auto* dir_ptr : {&rsrc_vendor_dir, &vendor_dir}) {
for (const auto& e : boost::filesystem::directory_iterator(*dir_ptr)) {
if (e.path().extension().string() != ".json") continue;
if (!vv.contains(e.path().filename().string()))
return false;
}
}
m_ProfileJson = cache["profile_data"];
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide profile cache hit: " << path;
return true;
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: guide cache load failed: " << ex.what();
return false;
}
}
// Builds guide profile JSON from the bundled system preset cache
// (resources/profiles/system_presets_cache.cache, generated by CI from all vendor JSONs).
// This avoids the 90-second LoadProfileFamily fallback on first launch.
bool GuideFrame::BuildProfileDataFromBundledCache()
{
PresetBundleCache::SystemPresetsCache cache;
if (!cache.load(PresetBundleCache::SystemPresetsCache::bundled_cache_path()))
return false;
if (!cache.is_plausible())
return false;
// Validate version strings against the current resources/profiles/ directory.
if (!cache.is_valid(rsrc_vendor_dir.string()))
return false;
try {
// Models from cached vendor profiles
for (const auto& cvp : cache.vendor_profiles) {
for (const auto& cm : cvp.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" / cvp.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"] = cvp.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 : cache.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 : cache.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 : cache.print_presets) {
if (!cp.is_visible) continue;
json entry;
entry["name"] = cp.name;
entry["sub_path"] = cp.file;
m_ProfileJson["process"].push_back(entry);
}
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from bundled system 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;
}
}
void GuideFrame::save_guide_cache() const
{
try {
json cache;
// Record version strings (not mtimes) so the cache is portable
// across machines and survives file extraction timestamp changes.
for (const auto* dir_ptr : {&rsrc_vendor_dir, &vendor_dir}) {
for (const auto& e : boost::filesystem::directory_iterator(*dir_ptr)) {
if (e.path().extension().string() != ".json") continue;
// Store version for all files, including version-less ones (empty string).
// try_load_guide_cache checks ALL json files, so blacklist.json etc. must be present.
cache["vendor_versions"][e.path().filename().string()] =
read_vendor_json_version(e.path());
}
}
cache["profile_data"] = m_ProfileJson;
boost::filesystem::create_directories(
boost::filesystem::path(guide_cache_path()).parent_path());
boost::nowide::ofstream ofs(guide_cache_path());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide profile cache saved to " << guide_cache_path();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: guide cache save failed: " << e.what();
}
}
int GuideFrame::LoadProfileData()
{
try {
@@ -1157,52 +1508,68 @@ int GuideFrame::LoadProfileData()
}
}
// 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);
//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;
// Resolve OrcaFilamentLibrary path regardless of whether we hit the cache
// (used later by GetFilamentInfo for custom filaments).
{
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();
}
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;
// Step 1: user guide JSON cache (~50ms, version-string validated)
bool cache_hit = try_load_guide_cache();
if (!cache_hit) {
if (!BuildProfileDataFromPresetBundle()) {
// Step 2: live preset bundle covers all rsrc vendors — done.
// Step 3: bundled system preset cache (CI-generated, ~1-2s)
bool slow_path = !BuildProfileDataFromBundledCache();
if (slow_path) {
// Step 4: 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);
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;
}
}
// After either bundled cache (~1-2s) or slow path (~90s), promote to
// user guide cache so every subsequent open takes ~50ms instead.
if (!m_destroy)
save_guide_cache();
}
if (m_destroy)
return 0;
}
wxGetApp().CallAfter([this] {

View File

@@ -78,6 +78,10 @@ public:
int LoadProfileData();
int SaveProfileData();
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromBundledCache();
bool try_load_guide_cache();
void save_guide_cache() const;
int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);