Ship one preset cache per vendor in place of the profile JSONs

Each vendor's system presets serialize into a single <vendor>.opc built at
package time, and a shipped build carries that file alone — the profile JSON
and its sub-file tree are pruned. The vendor loader, the setup wizard's profile
list and the resource installer all read a vendor through its cache, falling
back to parsing whenever one is absent, stale or unreadable, so the cache stays
an optimization and never a source of truth. Caches hold presets in source form
and resolve inheritance at load, through the same code the JSON path uses.
This commit is contained in:
SoftFever
2026-08-05 21:16:41 +08:00
parent 55fb4703c9
commit 8c050894db
26 changed files with 2675 additions and 1372 deletions

View File

@@ -115,8 +115,6 @@ 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

@@ -23,7 +23,7 @@ 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.
# generate_system_cache: pre-generates per-vendor <vendor>.opc files under resources/profiles 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})

View File

@@ -2,10 +2,10 @@
#include "libslic3r/Preset.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/algorithm/string/predicate.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;
@@ -58,31 +58,27 @@ int main(int argc, char* argv[])
auto preset_bundle = std::make_unique<PresetBundle>();
preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true);
preset_bundle->set_generate_vendor_caches(true);
std::cout << "Loading system presets from: " << profiles_path << "\n";
try {
// In validation mode data_dir() is the profiles directory set above, so the
// loader writes each <vendor>.opc next to its <vendor>.json as it parses it.
preset_bundle->load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent);
} catch (const std::exception& ex) {
std::cerr << "Failed to load presets: " << ex.what() << "\n";
return 1;
}
const std::string output_path =
(fs::path(profiles_path) / "system_presets.cache").make_preferred().string();
std::cout << "Saving single-bundle cache to: " << output_path << "\n";
const auto stats = preset_bundle->save_system_presets_cache(profiles_path, output_path);
if (!stats.ok) {
std::cerr << "ERROR: verification failed\n";
size_t cache_count = 0;
for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc"))
++ cache_count;
if (cache_count == 0) {
std::cerr << "No vendor cache files were generated under " << profiles_path << "\n";
return 1;
}
std::cout << "[ok] system_presets.cache\n"
<< " Total print presets: " << stats.print_presets << "\n"
<< " Total filament presets: " << stats.filament_presets << "\n"
<< " Total printer presets: " << stats.printer_presets << "\n";
std::cout << "Generated " << cache_count << " vendor cache file(s) under " << profiles_path << "\n";
return 0;
}

View File

@@ -28,6 +28,9 @@
#include <cereal/access.hpp>
#include <cereal/types/base_class.hpp>
// The serialize() members below archive ConfigOption hierarchies through
// cereal::base_class, whose registration machinery lives in polymorphic.hpp.
#include <cereal/types/polymorphic.hpp>
namespace Slic3r {
struct FloatOrPercent

View File

@@ -152,17 +152,6 @@ Semver get_version_from_json(std::string file_path)
}
}
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
int get_values_from_json(std::string file_path, std::vector<std::string>& keys, std::map<std::string, std::string>& key_values)
{
@@ -275,18 +264,28 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
}
};
// The four variant sets are immutable after static init and probed for every
// key of every preset loaded; one merged map makes that a single lookup.
// emplace keeps the first insertion, preserving the first-set-wins priority
// of the else-if chain this replaces.
static const std::unordered_map<std::string, int> variant_class = [] {
std::unordered_map<std::string, int> m;
for (const std::string& k : print_options_with_variant) m.emplace(k, 0);
for (const std::string& k : filament_options_with_variant) m.emplace(k, 1);
for (const std::string& k : printer_options_with_variant_1) m.emplace(k, 2);
for (const std::string& k : printer_options_with_variant_2) m.emplace(k, 3);
return m;
}();
for(auto& key :config.keys()){
if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){
replace_nil_and_resize(key, process_variant_length);
}
else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){
replace_nil_and_resize(key, filament_variant_length);
}
else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){
replace_nil_and_resize(key, machine_variant_length);
}
else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){
replace_nil_and_resize(key, machine_variant_length * 2);
auto iter = variant_class.find(key);
if (iter == variant_class.end())
continue;
switch (iter->second) {
case 0: replace_nil_and_resize(key, process_variant_length); break;
case 1: replace_nil_and_resize(key, filament_variant_length); break;
case 2: replace_nil_and_resize(key, machine_variant_length); break;
case 3: replace_nil_and_resize(key, machine_variant_length * 2); break;
}
}
}
@@ -758,7 +757,6 @@ 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,14 +16,6 @@
#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"
@@ -122,10 +114,6 @@ 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);
@@ -144,8 +132,9 @@ public:
PrinterVariant(const std::string &name) : name(name) {}
std::string name;
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive>
void serialize(Archive& ar) { ar(name); }
void serialize(Archive& ar) { ar(name); } // PrinterVariant
};
struct PrinterModel {
@@ -154,7 +143,7 @@ public:
std::string name;
//BBS: this is internal id for the printer. Currently only used for searching in database
std::string model_id;
PrinterTechnology technology;
PrinterTechnology technology = ptFFF;
std::string family;
std::vector<PrinterVariant> variants;
std::vector<std::string> default_materials;
@@ -178,13 +167,15 @@ public:
const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive>
void serialize(Archive& ar)
void serialize(Archive& ar) // PrinterModel
{
ar(id, name, model_id, family, technology, variants, default_materials,
ar(id, name, model_id, technology, family, 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);
bottom_texture_rect, bottom_texture_rect_longer, middle_texture_rect,
hotend_model);
}
};
std::vector<PrinterModel> models;
@@ -197,10 +188,11 @@ public:
bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); }
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
template<class Archive>
void serialize(Archive& ar)
void serialize(Archive& ar) // VendorProfile
{
ar(id, name, config_version, config_update_url, changelog_url,
ar(name, id, config_version, config_update_url, changelog_url,
models, default_filaments, default_sla_materials);
}
@@ -456,31 +448,12 @@ 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) {}
// Default constructor is public so cereal can default-construct elements when
// deserializing std::vector<Preset> (std::allocator is not a cereal::access friend).
Preset() = default;
protected:
friend class PresetCollection;
friend class PresetBundle;
friend class cereal::access;
// Serializes all value fields of Preset for the binary vendor cache.
// Raw pointers (vendor, loading_substitutions) are excluded — vendor is reconstructed
// by apply_vendor_cache() from the VendorProfile stored alongside the presets.
template<class Archive>
void serialize(Archive& ar)
{
ar(type, name, alias, file, version,
filament_id, setting_id, description,
renamed_from, is_system, is_visible,
is_default, is_external, is_dirty, is_compatible,
is_project_embedded, loaded,
m_from_orca_filament_lib, m_excluded_from,
bundle_id, user_id, base_id, sync_info,
updated_time, key_values, ini_str,
config);
}
Preset() = default;
};
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@
#include "enum_bitmask.hpp"
#include <memory>
#include <set>
#include <shared_mutex>
#include <unordered_map>
#include <optional>
@@ -13,7 +14,6 @@
#include <boost/filesystem/path.hpp>
#include <unordered_set>
#define DEFAULT_USER_FOLDER_NAME "default"
#define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json"
@@ -171,31 +171,72 @@ struct PresetBundleMetadata
class PresetBundle
{
public:
// ---- System presets single-bundle cache --------------------------------
// All vendor profiles + all system presets in one file.
// Bundled (CI): resources/profiles/system_presets.cache
// User runtime: data_dir/system/system_presets.cache
// ---- Per-vendor preset cache --------------------------------------------
// One cache file per vendor (plus the Orca filament library), stamped with
// the vendor's own profile version rather than a directory scan.
struct SaveCacheResult {
bool ok = false;
size_t print_presets = 0;
size_t filament_presets = 0;
size_t printer_presets = 0;
// The cache is not something a caller loads from: a vendor is loaded with
// load_vendor_configs_from_json, which comes from the cache whenever one covers
// it. What is public here is what the cache's own tests drive directly.
// One preset as its JSON subfile states it: the config diff, the name of the
// preset it inherits, and the parse metadata — everything the parse phase of
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance
// is resolved when the entry is installed, against whatever filament library
// is loaded then, so a cache carries no other vendor's values and no other
// vendor's update can make it stale.
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
struct CachedPreset
{
std::string name;
std::string sub_path; // path under the vendor's directory
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
std::string inherits;
std::string description;
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
std::string setting_id;
std::string filament_id;
std::vector<std::string> renamed_from;
template<class Archive>
void serialize(Archive& ar)
{
ar(name, sub_path, config_src, inherits, description, instantiation,
setting_id, filament_id, renamed_from);
}
};
static std::string bundled_system_presets_cache_path();
static std::string user_system_presets_cache_path();
// Save one vendor (vendor_name at vendor_version): its vendor profile and its
// presets in source form, plus how many errors their parse counted.
static bool save_vendor_cache(const std::string& cache_path, const std::string& vendor_name,
const std::string& vendor_version, const VendorMap& vendors,
const std::vector<CachedPreset>& process_entries,
const std::vector<CachedPreset>& filament_entries,
const std::vector<CachedPreset>& machine_entries,
uint64_t parse_errors);
// Capture the currently-loaded PresetBundle and write a single-bundle cache
// to output_path. profiles_dir contains the vendor JSON files (for the key).
// Used by generate_system_cache and tests.
SaveCacheResult save_system_presets_cache(const std::string& profiles_dir,
const std::string& output_path) const;
// Load a validated per-vendor cache into this bundle by installing its
// entries, with base_bundle's filament library as the inheritance base.
// Rejects (returns false, with this bundle left clean) unless the cache
// version, schema fingerprint and vendor name match, the cache was built
// from a vendor profile at least as new as the expected one, and every
// entry installs. An invalid expected version (a profile whose version
// cannot be judged) is never served from cache; Semver::inf() (no profile
// beside the cache at all) accepts whatever is cached.
bool load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name,
const Semver& expected_vendor_version, const PresetBundle* base_bundle = nullptr);
// Load a single-bundle cache into out_bundle for the guide wizard.
// Returns false if the cache is missing, stale, or corrupt.
static bool load_system_presets_cache_for_guide(const std::string& cache_path,
PresetBundle& out_bundle);
// Read the profile version a cache was stamped with, without deserializing its
// presets. Empty if the file is unreadable, not a cache this build understands,
// or not this vendor's. This is how an installed vendor's version is known when
// only its cache is installed.
static std::string peek_vendor_cache_version(const std::string& cache_path, const std::string& expected_vendor_name);
// Enable writing a per-vendor cache after a JSON parse (off by default). Cache
// content is pure parse output, so the guard is policy, not correctness: only
// the deliberate generators (load_system_presets_from_json, the cache build
// tool) write files, not every incidental load a dialog performs.
void set_generate_vendor_caches(bool enable) { m_generate_vendor_caches = enable; }
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
Preset &in_print_preset,
@@ -471,8 +512,12 @@ public:
/*std::pair<PresetsConfigSubstitutions, size_t> load_configbundle(
const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
//Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance
// Orca: `dir` is where the vendor is looked for — its own directory, whether or
// not the profile JSONs are still there. A whole-vendor load comes from the
// vendor's preset cache whenever one covers the profile on disk, and is parsed
// from the JSONs (falling back to the ones in resources) only when none does.
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
// Export a config bundle file containing all the presets and the names of the active presets.
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
@@ -544,19 +589,55 @@ public:
// Orca: for validation only.
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
// Errors the last load recorded. What the cache's error accounting promises —
// a cache-served vendor reports what its parse would — is pinned against this.
int error_count() const { return m_errors; }
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
bool check_preset_references() const;
// Merge one vendor's presets with the other vendor's presets, report duplicates.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
std::vector<std::string> merge_presets(PresetBundle &&other);
private:
// Compute combined invalidation key from all vendor JSON files in system_dir.
static std::string compute_system_presets_cache_key(const std::string& system_dir);
// Try user cache then bundled cache; apply all presets on hit.
bool try_load_system_presets_from_cache(const std::string& expected_key);
// Load one vendor from its preset cache: the one in `dir`, or — when that is
// missing or stale — the one shipped in resources/profiles, both judged against
// the vendor as installed in `dir`. False, with this bundle left clean, when
// neither is usable and the vendor has to be parsed. This is how
// load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Read raw cache blob: verify magic, size, CRC.
static bool read_cache_blob(const std::string& path, std::string& out_blob);
// Write a cache blob with the standard 20-byte file header.
static void write_cache_blob(const std::string& path, const std::string& blob);
// Write a cache blob with the standard 20-byte file header. False when the
// file could not be opened or written whole.
static bool write_cache_blob(const std::string& path, const std::string& blob);
// Install one source-form preset entry into this bundle: resolve `inherits`,
// flatten, validate and register the preset. Returns the reason installation
// failed, empty on success. See the definition for the sharing contract
// between the JSON parse and the cache load.
// retain_configs, when non-null, names the only presets registered into
// config_maps (a full config copy each). The cache load passes the names its
// entries inherit — the only ones ever looked up again; the JSON parse
// retains all, not knowing what later subfiles inherit.
std::string install_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr);
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
void clear_printer_hold_aliases();
// Whether to (re)write a per-vendor cache after a JSON parse.
bool m_generate_vendor_caches { false };
// Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match).
@@ -565,8 +646,6 @@ private:
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
//BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);
// Merge one vendor's presets with the other vendor's presets, report duplicates.
std::vector<std::string> merge_presets(PresetBundle &&other);
// Update the multicolor information for filaments.
void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles.
@@ -601,6 +680,38 @@ private:
ENABLE_ENUM_BITMASK_OPERATORS(PresetBundle::LoadConfigBundleAttribute)
// True if `vendor` is installed in data_dir()/system. A build that ships preset
// caches installs the cache alone, so it — not the profile — marks a vendor
// installed, and either one on its own counts.
extern bool is_vendor_installed(const std::string& vendor);
// The version of the installed vendor: what its profile claims, or what its cache
// was stamped with where only the cache is installed. Invalid Semver if neither is.
extern Semver installed_vendor_version(const std::string& vendor);
// Remove every form `vendor` can be installed as from data_dir()/system: its
// profile, its preset cache, and its preset directory.
extern void remove_installed_vendor(const std::string& vendor);
// The vendors `dir` holds, sorted: one is named by its profile or, in a build that
// ships preset caches instead of the raw profile JSONs, by its cache alone.
extern std::set<std::string> vendor_names_in(const boost::filesystem::path& dir);
// The version a build ships `vendor` at: whichever of its preset cache and its
// profile is newer, that being the one installing lays down. Invalid Semver if the
// build ships neither.
extern Semver resource_vendor_version(const std::string& vendor);
// Install vendors from the resources directory into the data directory, each as
// its preset cache or as its profile and preset JSONs — whichever of the two the
// build ships at the newer version. Anything the previous install of that vendor
// left behind goes, so only the form just installed is there to be loaded.
// bundle_names: vendor names, without extension.
// Returns false on the first vendor that cannot be installed.
extern bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names,
const std::string& resource_subdir = "profiles",
const std::string& data_subdir = "system");
} // namespace Slic3r
#endif /* slic3r_PresetBundle_hpp_ */

View File

@@ -197,7 +197,10 @@ public:
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);
auto v = Semver::parse(s);
if (! v)
throw std::runtime_error("Semver: cannot parse serialized version: " + s);
*this = std::move(*v);
}
private:

View File

@@ -722,15 +722,6 @@ void copy_directory_recursively(const boost::filesystem::path& source,
std::function<bool(const std::string)> filter = nullptr,
bool merge_mode = false);
// Install vendor bundles from resources directory to data directory
// bundle_names: vector of vendor bundle names (without .json extension)
// resource_subdir: subdirectory under resources_dir() (default: "profiles")
// data_subdir: subdirectory under data_dir() (default: "system")
// Returns: true if all bundles installed successfully, false otherwise
bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names,
const std::string& resource_subdir = "profiles",
const std::string& data_subdir = "system");
// Orca: Since 1.7.9 Boost deprecated save_string_file and load_string_file, copy and modified from boost 1.7.8
void save_string_file(const boost::filesystem::path& p, const std::string& str);
void load_string_file(const boost::filesystem::path& p, std::string& str);

View File

@@ -1724,76 +1724,6 @@ void copy_directory_recursively(const boost::filesystem::path& source,
return;
}
bool install_vendor_bundles_from_resources(
const std::vector<std::string>& bundle_names,
const std::string& resource_subdir,
const std::string& data_subdir)
{
namespace fs = boost::filesystem;
fs::path rsrc_path = fs::path(Slic3r::resources_dir()) / resource_subdir;
fs::path vendor_path = fs::path(Slic3r::data_dir()) / data_subdir;
BOOST_LOG_TRIVIAL(info) << "Installing " << bundle_names.size() << " bundles from resources...";
for (const auto &bundle : bundle_names) {
try {
// Install the JSON file
auto path_in_rsrc = (rsrc_path / bundle).replace_extension(".json");
auto path_in_vendors = (vendor_path / bundle).replace_extension(".json");
if (!fs::exists(path_in_rsrc)) {
BOOST_LOG_TRIVIAL(warning) << "Bundle not found in resources: " << bundle;
return false;
}
// Create target directory if needed
if (!fs::exists(vendor_path))
fs::create_directories(vendor_path);
// Copy JSON file
std::string error_message;
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
if (cfr != CopyFileResult::SUCCESS) {
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
return false;
}
// Copy the vendor directory (if it exists)
auto dir_in_rsrc = rsrc_path / bundle;
auto dir_in_vendors = vendor_path / bundle;
if (fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
// Remove existing directory
if (fs::exists(dir_in_vendors))
fs::remove_all(dir_in_vendors);
fs::create_directories(dir_in_vendors);
// Copy with file filter (same as PresetUpdater::install_bundles_rsrc)
// Filter out certain file types: .stl, .png, .svg, .jpeg, .jpg, .3mf
auto file_filter = [](const std::string name) -> bool {
return boost::iends_with(name, ".stl") ||
boost::iends_with(name, ".png") ||
boost::iends_with(name, ".svg") ||
boost::iends_with(name, ".jpeg") ||
boost::iends_with(name, ".jpg") ||
boost::iends_with(name, ".3mf");
};
copy_directory_recursively(dir_in_rsrc, dir_in_vendors, file_filter);
}
BOOST_LOG_TRIVIAL(info) << "Successfully installed bundle: " << bundle;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "Exception installing bundle " << bundle << ": " << e.what();
return false;
}
}
return true;
}
void save_string_file(const boost::filesystem::path& p, const std::string& str)
{
boost::nowide::ofstream file;

View File

@@ -432,14 +432,9 @@ const Snapshot& SnapshotDB::take_snapshot(const AppConfig &app_config, Snapshot:
cfg.models_variants_installed.erase(it ++);
else
++ it;
// Read the active config bundle, parse the config version.
PresetBundle bundle;
//BBS: change directoties by design
//bundle.load_configbundle((data_dir / PRESET_SYSTEM_DIR / (cfg.name + ".ini")).string(), PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
bundle.load_vendor_configs_from_json((data_dir/PRESET_SYSTEM_DIR).string(), cfg.name, PresetBundle::LoadConfigBundleAttribute::LoadVendorOnly, ForwardCompatibilitySubstitutionRule::EnableSilent);
for (const auto &vp : bundle.vendors)
if (vp.second.id == cfg.name)
cfg.version.config_version = vp.second.config_version;
// Orca: the version the vendor is installed at, read from its profile or —
// where the cache is the whole installation — from the cache's own stamp.
cfg.version.config_version = installed_vendor_version(cfg.name);
snapshot.vendor_configs.emplace_back(std::move(cfg));
}

View File

@@ -66,41 +66,41 @@ using Config::SnapshotDB;
// Configuration data structures extensions needed for the wizard
//BBS: set BBL as default
bool Bundle::load(fs::path source_path, bool ais_in_resources, bool ais_bbl_bundle)
bool Bundle::load(fs::path dir, const std::string &vendor_name, bool ais_in_resources, bool ais_bbl_bundle)
{
this->preset_bundle = std::make_unique<PresetBundle>();
this->is_in_resources = ais_in_resources;
this->is_bbl_bundle = ais_bbl_bundle;
std::string path_string = source_path.string();
std::string parent_path = source_path.parent_path().string();
//BBS: add json logic for vendor bundles
std::string vendor_name = source_path.filename().string();
if (Slic3r::is_json_file(path_string)) {
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
}
else
// Orca: served from the vendor's preset cache where one covers it — which is
// how a shipped build carries its vendors — and parsed from the JSONs otherwise.
// A vendor that can be neither read nor parsed — a cache the build cannot use
// with the preset JSONs behind it pruned, say — is one the wizard cannot offer.
// Every other vendor still can be, so it is left out rather than thrown over.
size_t presets_loaded = 0;
try {
auto [config_substitutions, loaded] = preset_bundle->load_vendor_configs_from_json(
dir.string(), vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
UNUSED(config_substitutions);
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
assert(config_substitutions.empty());
presets_loaded = loaded;
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(fatal) << boost::format("Vendor bundle: `%1%`: cannot be loaded, leaving it out: %2%") % vendor_name % e.what();
return false;
// Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air.
//BBS: add json logic for vendor bundles
auto [config_substitutions, presets_loaded] = preset_bundle->load_vendor_configs_from_json(
parent_path, vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
UNUSED(config_substitutions);
// No substitutions shall be reported when loading a system config bundle, no substitutions are allowed.
assert(config_substitutions.empty());
}
auto first_vendor = preset_bundle->vendors.begin();
if (first_vendor == preset_bundle->vendors.end()) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % path_string;
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No vendor information defined, cannot install.") % vendor_name;
return false;
}
if (presets_loaded == 0) {
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % path_string;
BOOST_LOG_TRIVIAL(error) << boost::format("Vendor bundle: `%1%`: No profile loaded.") % vendor_name;
return false;
}
}
BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % path_string % presets_loaded;
BOOST_LOG_TRIVIAL(trace) << boost::format("Vendor bundle: `%1%`: %2% profiles loaded.") % vendor_name % presets_loaded;
this->vendor_profile = &first_vendor->second;
return true;
}
@@ -125,15 +125,10 @@ BundleMap BundleMap::load()
//Orca: add custom as default
//Orca: add json logic for vendor bundle
auto orca_bundle_path = (vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
auto orca_bundle_rsrc = false;
if (!boost::filesystem::exists(orca_bundle_path)) {
orca_bundle_path = (rsrc_vendor_dir / PresetBundle::ORCA_DEFAULT_BUNDLE).replace_extension(".json");
orca_bundle_rsrc = true;
}
{
const bool from_rsrc = ! is_vendor_installed(PresetBundle::ORCA_DEFAULT_BUNDLE);
Bundle bbl_bundle;
if (bbl_bundle.load(std::move(orca_bundle_path), orca_bundle_rsrc, true))
if (bbl_bundle.load(from_rsrc ? rsrc_vendor_dir : vendor_dir, PresetBundle::ORCA_DEFAULT_BUNDLE, from_rsrc, true))
res.emplace(PresetBundle::ORCA_DEFAULT_BUNDLE, std::move(bbl_bundle));
}
@@ -141,18 +136,13 @@ BundleMap BundleMap::load()
// and then additionally from resources/profiles.
bool is_in_resources = false;
for (auto dir : { &vendor_dir, &rsrc_vendor_dir }) {
for (const auto &dir_entry : boost::filesystem::directory_iterator(*dir)) {
//BBS: add json logic for vendor bundle
if (Slic3r::is_json_file(dir_entry.path().string())) {
std::string id = dir_entry.path().stem().string(); // stem() = filename() without the trailing ".json" part
for (const std::string &id : vendor_names_in(*dir)) {
// Don't load this bundle if we've already loaded it.
if (res.find(id) != res.end()) { continue; }
// Don't load this bundle if we've already loaded it.
if (res.find(id) != res.end()) { continue; }
Bundle bundle;
if (bundle.load(dir_entry.path(), is_in_resources))
res.emplace(std::move(id), std::move(bundle));
}
Bundle bundle;
if (bundle.load(*dir, id, is_in_resources))
res.emplace(id, std::move(bundle));
}
is_in_resources = true;

View File

@@ -71,9 +71,11 @@ struct Bundle
Bundle() = default;
Bundle(Bundle&& other);
// Load the vendor `vendor_name` as it is installed in `dir`, from its preset
// cache or its profile JSONs, whichever is usable.
// Returns false if not loaded. Reason for that is logged as boost::log error.
//BBS: set BBL as default
bool load(fs::path source_path, bool is_in_resources, bool is_bbl_bundle = false);
bool load(fs::path dir, const std::string &vendor_name, bool is_in_resources, bool is_bbl_bundle = false);
const std::string& vendor_id() const { return vendor_profile->id; }
};

View File

@@ -2201,25 +2201,14 @@ bool CreatePrinterPresetDialog::load_system_and_user_presets_with_curr_model(Pre
} else {
selected_vendor_id = m_printer_preset_vendor_selected.id;
if (boost::filesystem::exists(boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR / selected_vendor_id)) {
preset_path = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string();
} else if (boost::filesystem::exists(boost::filesystem::path(Slic3r::resources_dir()) / "profiles" / selected_vendor_id)) {
preset_path = (boost::filesystem::path(Slic3r::resources_dir()) / "profiles").string();
}
if (preset_path.empty()) {
BOOST_LOG_TRIVIAL(info) << "Preset path was not found";
MessageDialog dlg(this, _L("Preset path was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES_NO | wxYES_DEFAULT | wxCENTRE);
dlg.ShowModal();
return false;
}
try {
// Pass the app's preset bundle (which already holds OrcaFilamentLibrary) as the base
// bundle so vendor filaments that inherit OFL bases resolve via the existing
// cross-vendor inheritance path.
temp_preset_bundle.load_vendor_configs_from_json(preset_path, selected_vendor_id,
// Orca: served from the vendor's preset cache where one covers it — a shipped
// build carries that instead of the raw preset JSONs — and parsed otherwise.
temp_preset_bundle.load_vendor_configs_from_json((boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).string(),
selected_vendor_id,
PresetBundle::LoadConfigBundleAttribute::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent,
wxGetApp().preset_bundle);

View File

@@ -1,6 +1,7 @@
#include "WebGuideDialog.hpp"
#include "ConfigWizard.hpp"
#include <boost/algorithm/string/join.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/filesystem/path.hpp>
@@ -43,33 +44,6 @@ using namespace nlohmann;
namespace Slic3r { namespace GUI {
static std::string guide_json_cache_version_key(const boost::filesystem::path& rsrc_dir,
const boost::filesystem::path& user_dir)
{
std::vector<std::string> parts;
auto collect = [&](const boost::filesystem::path& dir) {
boost::system::error_code ec;
if (!boost::filesystem::exists(dir, ec)) return;
for (const auto& e : boost::filesystem::directory_iterator(dir, ec)) {
if (e.path().extension().string() != ".json") continue;
const std::string k = get_vendor_cache_key(e.path().string());
if (!k.empty())
parts.push_back(e.path().filename().string() + "=" + k);
}
};
collect(rsrc_dir);
if (user_dir != rsrc_dir) collect(user_dir);
std::sort(parts.begin(), parts.end());
std::string result;
for (const auto& p : parts) { result += p; result += ';'; }
return result;
}
static boost::filesystem::path guide_json_cache_path()
{
return boost::filesystem::path(data_dir()) / "guide_profile_cache.json";
}
static wxString update_custom_filaments()
{
json m_Res = json::object();
@@ -217,8 +191,7 @@ GuideFrame::GuideFrame(GUI_App *pGUI, long style)
GuideFrame::~GuideFrame()
{
m_destroy = true;
*m_cancel_token = true; // signal any queued CallAfter lambdas before join
*m_cancel_token = true; // stop the loading thread and any queued CallAfter lambdas before join
if (m_load_task && m_load_task->joinable())
m_load_task->join();
m_load_task.reset();
@@ -327,13 +300,19 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt)
/**
* Callback invoked when a navigation request was accepted
*/
void GuideFrame::init_guide_paths()
// The empty shape every profile-loading path starts from or falls back to.
void GuideFrame::reset_profile_json()
{
m_ProfileJson = json::parse("{}");
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
}
void GuideFrame::init_guide_paths()
{
m_ProfileJson = json::parse("{}");
reset_profile_json();
vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred();
@@ -376,27 +355,7 @@ void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt)
try {
init_guide_paths();
if (BuildProfileDataFromPresetBundle()) {
// Persist so future opens that start before preset_bundle is ready
// can skip the slower loading paths. Capture by value so the
// thread is safe even if the dialog closes before it finishes.
boost::thread([data = m_ProfileJson,
rsrc = rsrc_vendor_dir,
user = vendor_dir] {
try {
json cache;
cache["version"] = guide_json_cache_version_key(rsrc, user);
if (cache["version"].get<std::string>().empty()) return;
json base = data;
for (auto& entry : base["model"]) entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}).detach();
if (!m_destroy)
if (!*m_cancel_token)
on_profile_loaded();
} else {
// Presets not yet in memory — delegate to background thread.
@@ -858,11 +817,9 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
bool check_unsaved_preset_changes = false;
std::vector<std::string> install_bundles;
std::vector<std::string> remove_bundles;
const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred();
for (const auto &it : enabled_vendors) {
if (it.second.size() > 0) {
auto vendor_file = vendor_dir/(it.first + ".json");
if (!fs::exists(vendor_file)) {
if (!is_vendor_installed(it.first)) {
install_bundles.emplace_back(it.first);
}
}
@@ -873,8 +830,7 @@ bool GuideFrame::apply_config(AppConfig *app_config, PresetBundle *preset_bundle
if (it.second.size() > 0) {
if (enabled_vendors.find(it.first) != enabled_vendors.end())
continue;
auto vendor_file = vendor_dir/(it.first + ".json");
if (fs::exists(vendor_file)) {
if (is_vendor_installed(it.first)) {
remove_bundles.emplace_back(it.first);
}
}
@@ -1223,72 +1179,20 @@ int GuideFrame::GetFilamentInfo( std::string VendorDirectory, json & pFilaList,
return status;
}
bool GuideFrame::TryLoadGuideJsonCache()
bool GuideFrame::BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors)
{
const auto path = guide_json_cache_path();
boost::system::error_code ec;
if (!boost::filesystem::exists(path, ec)) return false;
try {
boost::nowide::ifstream ifs(path.string());
json cache;
ifs >> cache;
if (!cache.contains("version") || !cache.contains("data")) return false;
const std::string expected = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (expected.empty() || cache["version"].get<std::string>() != expected) return false;
m_ProfileJson = cache["data"];
if (m_ProfileJson["machine"].empty()) return false;
BOOST_LOG_TRIVIAL(info) << "GuideFrame: loaded profile data from guide JSON cache ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: guide JSON cache load failed: " << e.what();
return false;
}
}
void GuideFrame::SaveGuideJsonCache()
{
try {
json cache;
cache["version"] = guide_json_cache_version_key(rsrc_vendor_dir, vendor_dir);
if (cache["version"].get<std::string>().empty()) return;
json base = m_ProfileJson;
// Strip user-specific state — SaveProfileData() re-applies it from AppConfig.
for (auto& entry : base["model"])
entry["nozzle_selected"] = "";
cache["data"] = std::move(base);
boost::nowide::ofstream ofs(guide_json_cache_path().string());
ofs << cache.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << "GuideFrame: guide JSON cache saved";
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: failed to save guide JSON cache: " << e.what();
}
}
bool GuideFrame::BuildProfileDataFromPresetBundle()
{
PresetBundle* pb = wxGetApp().preset_bundle;
if (!pb || pb->vendors.empty())
return false;
try {
// Models from vendor profiles
for (const auto& [vendor_id, vp] : pb->vendors) {
for (const auto& [vendor_id, vp] : bundle.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;
}
const std::string materials_str = boost::algorithm::join(model.default_materials, ";");
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vendor_id / (model.id + "_cover.png"))
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (model.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
@@ -1298,7 +1202,7 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
json entry;
entry["model"] = model.id;
entry["name"] = model.name;
entry["vendor"] = vendor_id;
entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
@@ -1309,8 +1213,8 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
}
// Machine map: preset name -> {model, nozzle variant}
for (const Preset& p : pb->printers()) {
if (!p.is_system) continue;
for (const Preset& p : bundle.printers()) {
if (!p.is_system || !p.vendor) 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;
@@ -1322,8 +1226,9 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
}
// Filament map from system filament presets (vendor/type already resolved in config)
for (const Preset& p : pb->filaments()) {
if (!p.is_system) continue;
const json& machines = m_ProfileJson["machine"];
for (const Preset& p : bundle.filaments()) {
if (!p.is_system || !p.vendor) 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");
@@ -1334,9 +1239,10 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
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"];
auto it = machines.find(pname);
if (it != machines.end()) {
const std::string m = (*it)["model"];
const std::string n = (*it)["nozzle"];
model_list += "[" + m + "++" + n + "]";
}
}
@@ -1353,157 +1259,103 @@ bool GuideFrame::BuildProfileDataFromPresetBundle()
}
// Process list from visible system print presets
for (const Preset& p : pb->prints()) {
if (!p.is_system || !p.is_visible) continue;
for (const Preset& p : bundle.prints()) {
if (!p.is_system || !p.vendor || !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;
if (require_all_resource_vendors) {
// If rsrc_vendor_dir has vendors (profile JSONs, or the preset caches a
// packaged build ships instead) 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 the slow path reads both dirs.
try {
for (const std::string& name : vendor_names_in(rsrc_vendor_dir)) {
if (bundle.vendors.find(name) == bundle.vendors.end()) {
BOOST_LOG_TRIVIAL(info) << "GuideFrame: vendor '" << name
<< "' in resources but not in preset_bundle — falling back to JSON loading";
reset_profile_json();
return false;
}
}
}
} catch (const std::exception&) {}
} catch (const std::exception&) {}
}
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from preset_bundle ("
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data ("
<< 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()
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileJson 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();
reset_profile_json();
return false;
}
}
// Builds guide profile JSON from the per-vendor bundled caches
// (resources/profiles/system_presets.cache, generated by CI).
// This avoids the 90-second LoadProfileFamily fallback on first launch.
bool GuideFrame::BuildProfileDataFromBundledCache()
bool GuideFrame::BuildProfileDataFromPresetBundle()
{
const std::string cache_path = PresetBundle::bundled_system_presets_cache_path();
if (!boost::filesystem::exists(cache_path))
PresetBundle* pb = wxGetApp().preset_bundle;
if (!pb || pb->vendors.empty())
return false;
return BuildProfileJson(*pb, /*require_all_resource_vendors=*/true);
}
bool GuideFrame::BuildProfileDataFromVendors()
{
try {
// Same vendor set and precedence as the JSON scan in LoadProfileData: a
// vendor in the user's system dir shadows the bundled one of that name.
// A vendor is named by its profile or, where a build ships preset caches
// instead, by its cache alone — so both forms name one here.
std::map<std::string, boost::filesystem::path> vendor_files;
auto collect = [&vendor_files](const boost::filesystem::path& dir) {
boost::system::error_code ec;
if (!boost::filesystem::exists(dir, ec))
return;
for (const auto& e : boost::filesystem::directory_iterator(dir, ec))
if (Slic3r::is_json_file(e.path().string()) || e.path().extension() == ".opc")
vendor_files.emplace(e.path().stem().string(), e.path()); // first wins
};
collect(vendor_dir);
collect(rsrc_vendor_dir);
// Each vendor comes from its preset cache where one covers it, which is what
// makes this worth doing instead of the scan below; the filament library goes
// first because the others' filaments inherit from it, and resolving those on
// the vendors the cache does not cover needs it already loaded.
PresetBundle bundle;
if (!PresetBundle::load_system_presets_cache_for_guide(cache_path, bundle))
auto load_vendor = [this](PresetBundle& into, const std::string& vendor, const PresetBundle* base) {
into.load_vendor_configs_from_json(vendor_dir.string(), vendor, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
};
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
if (vendor_files.count(filament_library))
load_vendor(bundle, filament_library, nullptr);
for (const auto& entry : vendor_files) {
if (*m_cancel_token)
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
const std::string& vendor = entry.first;
// A cache is only ever written for a versioned vendor; a JSON has to be
// asked, so that an unversioned one (blacklist.json) carrying no presets
// is passed over.
if (vendor == filament_library ||
(entry.second.extension() != ".opc" && ! get_version_from_json(entry.second.string()).valid()))
continue;
PresetBundle tmp;
load_vendor(tmp, vendor, &bundle);
bundle.merge_presets(std::move(tmp));
}
if (bundle.vendors.empty())
return false;
for (const auto& [vendor_id, vp] : bundle.vendors) {
for (const auto& cm : vp.models) {
std::string nozzle_str;
for (const auto& v : cm.variants) {
if (!nozzle_str.empty()) nozzle_str += ";";
nozzle_str += v.name;
}
std::string materials_str;
for (const auto& m : cm.default_materials) {
if (!materials_str.empty()) materials_str += ";";
materials_str += m;
}
boost::filesystem::path cover_path =
(boost::filesystem::path(resources_dir()) / "profiles" / vp.id / (cm.id + "_cover.png"))
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path =
(boost::filesystem::path(resources_dir()) / "web/image/printer" / (cm.id + "_cover.png"))
.make_preferred();
json entry;
entry["model"] = cm.id;
entry["name"] = cm.name;
entry["vendor"] = vp.id;
entry["nozzle_diameter"] = nozzle_str;
entry["materials"] = materials_str;
entry["cover"] = cover_path.string();
entry["nozzle_selected"] = "";
entry["sub_path"] = "";
m_ProfileJson["model"].push_back(entry);
}
}
for (const Preset& cp : bundle.printers()) {
if (!cp.is_system || !cp.vendor) continue;
const auto* pm = cp.config.option<ConfigOptionString>("printer_model");
const auto* pv = cp.config.option<ConfigOptionString>("printer_variant");
if (!pm || pm->value.empty() || !pv) continue;
json mach;
mach["model"] = pm->value;
mach["nozzle"] = pv->value;
m_ProfileJson["machine"][cp.name] = mach;
}
for (const Preset& cp : bundle.filaments()) {
if (!cp.is_system || !cp.vendor) continue;
const auto* fv = cp.config.option<ConfigOptionStrings>("filament_vendor");
const auto* ft = cp.config.option<ConfigOptionStrings>("filament_type");
const auto* compat = cp.config.option<ConfigOptionStrings>("compatible_printers");
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;
}
for (const Preset& cp : bundle.prints()) {
if (!cp.is_system || !cp.vendor || !cp.is_visible) continue;
json entry;
entry["name"] = cp.name;
entry["sub_path"] = cp.file;
m_ProfileJson["process"].push_back(entry);
}
BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from single-bundle cache ("
<< m_ProfileJson["model"].size() << " models, "
<< m_ProfileJson["machine"].size() << " machines, "
<< m_ProfileJson["filament"].size() << " filaments)";
return !m_ProfileJson["machine"].empty();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache failed: " << ex.what();
m_ProfileJson["model"] = json::array();
m_ProfileJson["machine"] = json::object();
m_ProfileJson["filament"] = json::object();
m_ProfileJson["process"] = json::array();
return BuildProfileJson(bundle, /*require_all_resource_vendors=*/false);
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " failed: " << e.what();
reset_profile_json();
return false;
}
}
@@ -1512,55 +1364,48 @@ int GuideFrame::LoadProfileData()
{
// Background thread: the fast path in OnNavigationComplete failed (presets not yet loaded).
// Loading order (fastest to slowest):
// 1. Guide JSON cache (data_dir/guide_profile_cache.json, sub-second)
// 2. Bundled per-vendor binary caches (CI-generated, ~1-2s)
// 3. Read all vendor JSONs (~90s)
// After paths 2 or 3 the guide JSON cache is written so next open uses path 1.
// 1. Load every vendor, from its preset cache wherever one covers it
// 2. Read all vendor JSONs by hand
try {
if (!TryLoadGuideJsonCache()) {
if (!BuildProfileDataFromBundledCache()) {
// Last resort — read all vendor JSONs (~90s)
std::set<std::string> loaded_vendors;
auto filament_library_name = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json");
if (boost::filesystem::exists(vendor_dir / filament_library_name))
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (vendor_dir / filament_library_name).string());
else
LoadProfileFamily(PresetBundle::ORCA_FILAMENT_LIBRARY, (rsrc_vendor_dir / filament_library_name).string());
loaded_vendors.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
if (!BuildProfileDataFromVendors()) {
// Last resort — read all vendor JSONs
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);
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;
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_cancel_token) 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_cancel_token) return 0;
}
// Persist the result so subsequent opens skip both the bundled cache and
// the slow JSON loading path entirely.
SaveGuideJsonCache();
}
// Capture the cancel token by value (shared_ptr) so the lambda doesn't

View File

@@ -84,10 +84,10 @@ public:
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
void init_guide_paths();
void on_profile_loaded();
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromBundledCache();
bool TryLoadGuideJsonCache();
void SaveGuideJsonCache();
bool BuildProfileDataFromVendors();
void reset_profile_json();
int SaveProfile();
int GetFilamentInfo( std::string VendorDirectory,json & pFilaList, std::string filepath, std::string &sVendor, std::string &sType);
@@ -122,9 +122,9 @@ private:
//First Load
bool bFirstComplete{false};
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.
// Set once in the destructor. Read through `this` by the loading thread
// (joined before `this` dies) and captured as the shared_ptr by CallAfter
// lambdas so they don't touch `this` after 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;

View File

@@ -1044,46 +1044,42 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
std::set<std::string> bundles;
// Orca: always install filament library
bundles.insert(PresetBundle::ORCA_FILAMENT_LIBRARY);
for (auto &dir_entry : boost::filesystem::directory_iterator(rsrc_path)) {
const auto &path = dir_entry.path();
std::string file_path = path.string();
if (is_json_file(file_path)) {
const auto path_in_vendor = vendor_path / path.filename();
std::string vendor_name = path.filename().string();
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
if (bundles.find(vendor_name) != bundles.end())continue;
// A vendor is named by its profile or, where the build ships preset caches
// instead of the raw profile JSONs, by its cache alone.
for (const std::string &vendor_name : vendor_names_in(rsrc_path)) {
if (bundles.find(vendor_name) != bundles.end())continue;
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE
|| (enabled_vendors.find(vendor_name) != enabled_vendors.end());
if (enabled_config_update) {
if ( fs::exists(path_in_vendor)) {
if (is_vendor_enabled) {
Semver resource_ver = get_version_from_json(file_path);
Semver vendor_ver = get_version_from_json(path_in_vendor.string());
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE
|| (enabled_vendors.find(vendor_name) != enabled_vendors.end());
if (enabled_config_update) {
if (is_vendor_installed(vendor_name)) {
if (is_vendor_enabled) {
// Orca: whichever form of the vendor resources ships at the newer
// version is the one installing lays down, and the one to judge
// what is installed against.
Semver resource_ver = resource_vendor_version(vendor_name);
// Orca: a vendor installed as a preset cache has no profile
// beside it; the version it was installed at is in the cache.
Semver vendor_ver = installed_vendor_version(vendor_name);
if (vendor_ver < resource_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version "
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
bundles.insert(vendor_name);
}
}
else {
//need to be removed because not installed
fs::remove(path_in_vendor);
const auto path_of_vendor = vendor_path / vendor_name;
if (fs::exists(path_of_vendor))
fs::remove_all(path_of_vendor);
if (vendor_ver < resource_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:found vendor " << vendor_name << " newer version "
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
bundles.insert(vendor_name);
}
}
else if (is_vendor_enabled) {
bundles.insert(vendor_name);
else {
//need to be removed because not installed
remove_installed_vendor(vendor_name);
}
}
else if (is_vendor_enabled) {
bundles.insert(vendor_name);
}
}
else if (is_vendor_enabled) {
bundles.insert(vendor_name);
}
}
if (bundles.size() > 0) {
@@ -1163,11 +1159,12 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
if (( fs::exists(path_in_vendor))
if (is_vendor_installed(vendor_name)
|| fs::exists(print_in_cache)
|| fs::exists(filament_in_cache)
|| fs::exists(machine_in_cache)) {
Semver vendor_ver = get_version_from_json(path_in_vendor.string());
// Orca: a vendor installed as a preset cache carries its version there.
Semver vendor_ver = installed_vendor_version(vendor_name);
std::map<std::string, std::string> key_values;
std::vector<std::string> keys(3);