diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 8ea83314d7..275b02225c 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,9 +1,16 @@ #include #include #include +#include #include "PresetBundle.hpp" -#include "PresetBundleCache.hpp" + +#include +#include +#include +#include +#include +#include #include "PrintConfig.hpp" #include "libslic3r.h" #include "I18N.hpp" @@ -2195,60 +2202,84 @@ std::pair 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 partial_dirty_vendors; + // Per-vendor binary cache: try user cache, then bundled cache, then JSON parse. + // Vendors that hit a cache are applied immediately; misses go through JSON parsing below. + std::set cache_miss_vendors; + std::map vendor_json_versions; // vendor_id → version string on disk + 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 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::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); + this->reset(false); + bool all_from_cache = true; + + // Collect all vendor names from the system directory. + std::vector all_vendor_names; + try { + for (const auto& e : boost::filesystem::directory_iterator(dir)) { + if (Slic3r::is_json_file(e.path().string())) + all_vendor_names.push_back(e.path().stem().string()); } - // else: structural mismatch (format version or option count changed) → full re-parse. + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(warning) << "PresetBundle: cannot scan system dir: " << ex.what(); } - 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::steady_clock::now() - t0).count(); - BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from bundled cache in " << ms << " ms"; - return {PresetsConfigSubstitutions{}, ""}; - } + // Load ORCA_FILAMENT_LIBRARY first (other vendors' filaments inherit from it). + // Then load remaining vendors from per-vendor caches. + auto try_vendor_cache = [&](const std::string& vendor_name) -> bool { + const std::string json_path = (dir / (vendor_name + ".json")).string(); + const Semver ver = get_version_from_json(json_path); + const std::string ver_str = ver.valid() ? ver.to_string() : ""; + vendor_json_versions[vendor_name] = ver_str; + + VendorCache vc; + // 1. User per-vendor cache + if (vc.load(VendorCache::user_path(vendor_name)) && vc.is_valid(ver_str)) { + vc.apply(*this); + return true; + } + // 2. Bundled per-vendor cache (ships with installer) + if (vc.load(VendorCache::bundled_path(vendor_name)) && vc.is_valid(ver_str)) { + vc.apply(*this); + vc.save(VendorCache::user_path(vendor_name)); // promote + return true; + } + return false; + }; + + // Sort: ORCA_FILAMENT_LIBRARY goes first, rest alphabetical. + std::sort(all_vendor_names.begin(), all_vendor_names.end(), + [](const std::string& a, const std::string& b) { + if (a == ORCA_FILAMENT_LIBRARY) return true; + if (b == ORCA_FILAMENT_LIBRARY) return false; + return a < b; + }); + + for (const auto& name : all_vendor_names) { + if (!try_vendor_cache(name)) { + cache_miss_vendors.insert(name); + all_from_cache = false; } - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cache miss, falling back to JSON load"; } + + if (all_from_cache && !all_vendor_names.empty()) { + update_system_maps(); + const auto ms = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets loaded from per-vendor cache in " << ms << " ms"; + return {PresetsConfigSubstitutions{}, ""}; + } + if (!cache_miss_vendors.empty()) + BOOST_LOG_TRIVIAL(info) << "PresetBundle: " << cache_miss_vendors.size() + << " vendor(s) need JSON parse: " + << [&]{ std::string s; for (auto& v : cache_miss_vendors) s += v + " "; return s; }(); } const auto json_load_t0 = std::chrono::steady_clock::now(); PresetsConfigSubstitutions substitutions; std::string errors_cummulative; - bool first = partial_dirty_vendors.empty(); // false in partial mode: clean vendors already applied + // first = true means no vendor has been loaded yet (from cache or JSON). + // false = at least one vendor was applied from the per-vendor cache above. + bool first = validation_mode || cache_miss_vendors.size() == vendor_json_versions.size(); std::vector vendor_names; // store all vendor names in vendor_names for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) { @@ -2270,8 +2301,8 @@ std::pair PresetBundle::load_system_pre std::vector 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)) + // Skip vendors already loaded from the per-vendor cache. + if (!validation_mode && !cache_miss_vendors.count(vn)) continue; if (vn == ORCA_FILAMENT_LIBRARY) orca_lib_vendor = vn; @@ -2359,15 +2390,20 @@ std::pair PresetBundle::load_system_pre 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()) { + // Save per-vendor binary caches for vendors that had to be parsed from JSON. + if (!validation_mode && !cache_miss_vendors.empty() && 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()); + for (const auto& vendor_name : cache_miss_vendors) { + const bool is_orca_lib = (vendor_name == ORCA_FILAMENT_LIBRARY); + const std::string ver_str = vendor_json_versions.count(vendor_name) + ? vendor_json_versions.at(vendor_name) : ""; + VendorCache vc; + vc.capture(*this, vendor_name, ver_str, is_orca_lib); + vc.save(VendorCache::user_path(vendor_name)); + } const auto save_ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - save_t0).count(); - BOOST_LOG_TRIVIAL(info) << "PresetBundle: system presets cache saved in " << save_ms << " ms"; + BOOST_LOG_TRIVIAL(info) << "PresetBundle: per-vendor caches saved in " << save_ms << " ms"; } //BBS: add config related logs @@ -5647,4 +5683,277 @@ bool BundleMetadata::save_to_json(const std::string& path) const return false; } } +// ---- VendorCache implementation ----------------------------------------- + +namespace { + +#pragma pack(push, 1) +struct CacheFileHeader { + uint32_t magic; + uint32_t version; + uint64_t data_size; + uint32_t crc32; +}; +#pragma pack(pop) +static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes"); + +template +static void save_blob(const std::string& path, const T& obj) +{ + std::ostringstream oss; + { + cereal::BinaryOutputArchive ar(oss); + ar(obj); + } + const std::string blob = oss.str(); + boost::crc_32_type crc; + crc.process_bytes(blob.data(), blob.size()); + try { + boost::filesystem::create_directories(boost::filesystem::path(path).parent_path()); + boost::nowide::ofstream ofs(path, std::ios::binary | std::ios::trunc); + if (!ofs.is_open()) { + BOOST_LOG_TRIVIAL(warning) << "VendorCache: cannot open for writing: " << path; + return; + } + CacheFileHeader hdr; + hdr.magic = VendorCache::CACHE_MAGIC; + hdr.version = VendorCache::CACHE_VERSION; + hdr.data_size = static_cast(blob.size()); + hdr.crc32 = crc.checksum(); + ofs.write(reinterpret_cast(&hdr), sizeof(hdr)); + ofs.write(blob.data(), static_cast(blob.size())); + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCache: write failed (" << path << "): " << e.what(); + } +} + +template +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(&hdr), sizeof(hdr))) + return false; + if (hdr.magic != VendorCache::CACHE_MAGIC || hdr.version != VendorCache::CACHE_VERSION) + return false; + if (hdr.data_size == 0 || hdr.data_size > 512u * 1024u * 1024u) + return false; + std::string blob(hdr.data_size, '\0'); + if (!ifs.read(&blob[0], static_cast(hdr.data_size))) + return false; + boost::crc_32_type crc; + crc.process_bytes(blob.data(), blob.size()); + if (crc.checksum() != hdr.crc32) { + BOOST_LOG_TRIVIAL(warning) << "VendorCache: CRC mismatch: " << path; + return false; + } + std::istringstream iss(blob); + cereal::BinaryInputArchive ar(iss); + ar(obj); + return true; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(warning) << "VendorCache: load failed (" << path << "): " << e.what(); + return false; + } +} + +} // anonymous namespace + +std::string VendorCache::user_path(const std::string& vendor_id) +{ + return (boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR / (vendor_id + ".cache")) + .make_preferred().string(); +} + +std::string VendorCache::bundled_path(const std::string& vendor_id) +{ + return (boost::filesystem::path(resources_dir()) / "profiles" / (vendor_id + ".cache")) + .make_preferred().string(); +} + +bool VendorCache::load(const std::string& path) +{ + return load_blob(path, *this); +} + +void VendorCache::save(const std::string& path) const +{ + save_blob(path, *this); +} + +void VendorCache::capture(const PresetBundle& bundle, + const std::string& vendor_id, + const std::string& vendor_json_ver, + bool capture_filament_maps) +{ + cache_version = CACHE_VERSION; + config_options_count = print_config_def.options.size(); + vendor_json_version = vendor_json_ver; + + print_presets.clear(); + filament_presets.clear(); + printer_presets.clear(); + sla_print_presets.clear(); + sla_material_presets.clear(); + config_maps.clear(); + filament_id_maps.clear(); + + // Vendor profile + auto vp_it = bundle.vendors.find(vendor_id); + if (vp_it != bundle.vendors.end()) { + const VendorProfile& vp = vp_it->second; + profile.id = vp.id; + profile.name = vp.name; + profile.config_version = vp.config_version.valid() ? vp.config_version.to_string() : ""; + profile.config_update_url = vp.config_update_url; + profile.changelog_url = vp.changelog_url; + profile.models.clear(); + 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(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; + profile.models.push_back(std::move(cm)); + } + profile.default_filaments.clear(); + for (const auto& f : vp.default_filaments) + profile.default_filaments.push_back(f); + profile.default_sla_materials.clear(); + for (const auto& m : vp.default_sla_materials) + profile.default_sla_materials.push_back(m); + } + + // Presets — only those belonging to this vendor + auto capture_col = [&](const PresetCollection& coll, std::vector& out) { + for (const Preset& p : coll()) { + if (!p.is_system) continue; + if (p.vendor == nullptr || p.vendor->id != vendor_id) continue; + CachedPreset cp; + cp.type = static_cast(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 = 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); + + if (capture_filament_maps) { + config_maps = bundle.m_config_maps; + filament_id_maps = bundle.m_filament_id_maps; + } +} + +void VendorCache::apply(PresetBundle& bundle) const +{ + // Restore vendor profile (additive — does not reset the bundle) + { + VendorProfile vp(profile.id); + vp.name = profile.name; + vp.config_update_url = profile.config_update_url; + vp.changelog_url = profile.changelog_url; + if (!profile.config_version.empty()) { + auto v = Semver::parse(profile.config_version); + if (v) vp.config_version = *v; + } + for (const auto& cm : profile.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(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 : profile.default_filaments) + vp.default_filaments.insert(f); + for (const auto& m : profile.default_sla_materials) + vp.default_sla_materials.insert(m); + bundle.vendors.emplace(profile.id, std::move(vp)); + } + + // Restore presets + auto apply_col = [&](const std::vector& cached, + PresetCollection& coll, + bool is_filaments) { + for (const auto& cp : cached) { + 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); + + if (!config_maps.empty()) { + bundle.m_config_maps = config_maps; + bundle.m_filament_id_maps = filament_id_maps; + } +} + } // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 220de95b78..0eb29918a6 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -13,6 +13,13 @@ #include #include +#include +#include +#include +#include +#include +#include + #define DEFAULT_USER_FOLDER_NAME "default" #define BUNDLE_STRUCTURE_JSON_NAME "bundle_structure.json" @@ -147,6 +154,126 @@ struct PresetBundleMetadata } }; +// ---- Per-vendor binary preset cache ---------------------------------------- +// One file per vendor: +// Bundled (CI-generated): resources/profiles/.cache +// User (runtime): data_dir/system/.cache + +struct CachedPrinterVariant { + std::string name; + template void serialize(Archive& ar) { ar(name); } +}; + +struct CachedPrinterModel { + std::string id, name, model_id, family; + int technology = 0; + std::vector variants; + std::vector default_materials; + std::vector 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 + 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 models; + std::vector default_filaments; + std::vector default_sla_materials; + + template + void serialize(Archive& ar) + { + ar(id, name, config_version, config_update_url, changelog_url, + models, default_filaments, default_sla_materials); + } +}; + +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 renamed_from; + bool is_system = true; + bool is_visible = true; + bool m_from_orca_filament_lib = false; + DynamicPrintConfig config; + + template + 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); + } +}; + +struct VendorCache { + static constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ" + static constexpr uint32_t CACHE_VERSION = 3; + + uint32_t cache_version = CACHE_VERSION; + size_t config_options_count = 0; + std::string vendor_json_version; // Semver string, or "" for version-less vendors + + CachedVendorProfile profile; + std::vector print_presets; + std::vector filament_presets; + std::vector printer_presets; + std::vector sla_print_presets; + std::vector sla_material_presets; + + // Only populated for the ORCA_FILAMENT_LIBRARY vendor. + std::map config_maps; + std::map filament_id_maps; + + template + void serialize(Archive& ar) + { + ar(cache_version, config_options_count, vendor_json_version, + profile, + print_presets, filament_presets, printer_presets, + sla_print_presets, sla_material_presets, + config_maps, filament_id_maps); + } + + // True when this cache entry is current and can be used without re-parsing JSON. + bool is_valid(const std::string& current_vendor_json_version) const + { + return cache_version == CACHE_VERSION + && config_options_count == print_config_def.options.size() + && vendor_json_version == current_vendor_json_version; + } + + static std::string user_path(const std::string& vendor_id); // data_dir/system/.cache + static std::string bundled_path(const std::string& vendor_id); // resources/profiles/.cache + + bool load(const std::string& path); + void save(const std::string& path) const; + + // Capture one vendor's data from an already-loaded PresetBundle. + // Set capture_filament_maps=true only for the ORCA_FILAMENT_LIBRARY vendor. + void capture(const PresetBundle& bundle, + const std::string& vendor_id, + const std::string& vendor_json_version, + bool capture_filament_maps); + + // Apply this vendor's data into bundle (additive — does NOT reset the bundle). + void apply(PresetBundle& bundle) const; +}; + // Bundle of Print + Filament + Printer presets. class PresetBundle { diff --git a/src/libslic3r/PresetBundleCache.cpp b/src/libslic3r/PresetBundleCache.cpp deleted file mode 100644 index 9503948b6e..0000000000 --- a/src/libslic3r/PresetBundleCache.cpp +++ /dev/null @@ -1,346 +0,0 @@ -#include "PresetBundleCache.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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 -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(blob.size()); - hdr.crc32 = crc.checksum(); - ofs.write(reinterpret_cast(&hdr), sizeof(hdr)); - ofs.write(blob.data(), static_cast(blob.size())); - } catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(warning) << "PresetBundleCache: write failed (" << path << "): " << e.what(); - } -} - -template -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(&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(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 dummy; - return get_dirty_vendors(system_dir, dummy) && dummy.empty(); -} - -bool SystemPresetsCache::get_dirty_vendors(const std::string& system_dir, std::set& out_dirty) const -{ - out_dirty.clear(); - if (format_version != FORMAT_VERSION || config_options_count != print_config_def.options.size()) - return false; - std::map 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(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& out) { - for (const Preset& p : coll()) { - if (!p.is_system) - continue; - CachedPreset cp; - cp.type = static_cast(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& 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(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& 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 diff --git a/src/libslic3r/PresetBundleCache.hpp b/src/libslic3r/PresetBundleCache.hpp deleted file mode 100644 index b2ddbc31b6..0000000000 --- a/src/libslic3r/PresetBundleCache.hpp +++ /dev/null @@ -1,145 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "PrintConfig.hpp" - -namespace Slic3r { - -class PresetBundle; - -namespace PresetBundleCache { - -// ---- Vendor profile structures ---- - -struct CachedPrinterVariant { - std::string name; - template void serialize(Archive& ar) { ar(name); } -}; - -struct CachedPrinterModel { - std::string id, name, model_id, family; - int technology = 0; - std::vector variants; - std::vector default_materials; - std::vector 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 - 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 models; - std::vector default_filaments; - std::vector default_sla_materials; - - template - 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 renamed_from; - bool is_system = true; - bool is_visible = true; - bool m_from_orca_filament_lib = false; - DynamicPrintConfig config; - - template - 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 /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 vendor_versions; - std::vector vendor_profiles; - - std::vector print_presets; - std::vector filament_presets; - std::vector printer_presets; - std::vector sla_print_presets; - std::vector sla_material_presets; - - std::map config_maps; - std::map filament_id_maps; - - template - 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& 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& skip_vendor_ids) const; - bool load(const std::string& path); - void save(const std::string& path) const; -}; - -} // namespace PresetBundleCache -} // namespace Slic3r \ No newline at end of file diff --git a/src/slic3r/GUI/WebGuideDialog.cpp b/src/slic3r/GUI/WebGuideDialog.cpp index 6cc1c6ca7a..e9cdb6e66b 100644 --- a/src/slic3r/GUI/WebGuideDialog.cpp +++ b/src/slic3r/GUI/WebGuideDialog.cpp @@ -10,8 +10,8 @@ #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Config.hpp" +#include "libslic3r/Preset.hpp" #include "libslic3r/PresetBundle.hpp" -#include "libslic3r/PresetBundleCache.hpp" #include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "libslic3r_version.h" @@ -43,8 +43,6 @@ using namespace nlohmann; namespace Slic3r { namespace GUI { -json m_ProfileJson; - static wxString update_custom_filaments() { json m_Res = json::object(); @@ -303,15 +301,73 @@ void GuideFrame::OnNavigationRequest(wxWebViewEvent &evt) /** * Callback invoked when a navigation request was accepted */ +void GuideFrame::init_guide_paths() +{ + m_ProfileJson = json::parse("{}"); + m_ProfileJson["model"] = json::array(); + m_ProfileJson["machine"] = json::object(); + m_ProfileJson["filament"] = json::object(); + m_ProfileJson["process"] = json::array(); + + vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); + rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); + orca_bundle_rsrc = true; + + if (boost::filesystem::exists(vendor_dir)) { + for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) { + if (!boost::filesystem::is_directory(entry) && + boost::iequals(entry.path().extension().string(), ".json") && + !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) { + orca_bundle_rsrc = false; + break; + } + } + } + + auto lib_json = boost::filesystem::path(PresetBundle::ORCA_FILAMENT_LIBRARY).replace_extension(".json"); + m_OrcaFilaLibPath = boost::filesystem::exists(vendor_dir / lib_json) + ? (vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string() + : (rsrc_vendor_dir / PresetBundle::ORCA_FILAMENT_LIBRARY).string(); +} + +void GuideFrame::on_profile_loaded() +{ + // Must be called on the main thread. + SaveProfileData(); + const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll; + json res; + res["command"] = "userguide_profile_load_finish"; + res["sequence_id"] = "10001"; + RunScript(wxString::Format("HandleStudio(%s)", res.dump(-1, ' ', true))); +} + void GuideFrame::OnNavigationComplete(wxWebViewEvent &evt) { //wxLogMessage("%s", "Navigation complete; url='" + evt.GetURL() + "'"); if (!bFirstComplete) { - m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this)); - // boost::thread LoadProfileThread(boost::bind(&GuideFrame::LoadProfileData, this)); - //LoadProfileThread.detach(); - bFirstComplete = true; + try { + // Steps 1 and 2 run on the main thread: safe to access preset_bundle and + // filesystem. Only fall through to the background thread for slow paths. + init_guide_paths(); + const bool cache_hit = try_load_guide_cache(); + if (cache_hit) { + if (!m_destroy) + on_profile_loaded(); + } else if (BuildProfileDataFromPresetBundle()) { + if (!m_destroy) { + save_guide_cache(); + on_profile_loaded(); + } + } else { + // Steps 3+4 are slow — delegate to background thread. + m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this)); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", init error: " << e.what(); + m_load_task = new boost::thread(boost::bind(&GuideFrame::LoadProfileData, this)); + } } m_browser->Show(); @@ -1272,30 +1328,6 @@ static std::string guide_cache_path() .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(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() @@ -1320,7 +1352,9 @@ bool GuideFrame::try_load_guide_cache() fp = vendor_dir / filename; if (!boost::filesystem::exists(fp)) return false; - if (read_vendor_json_version(fp) != cached_ver.get()) + const Semver disk_ver = get_version_from_json(fp.string()); + const std::string disk_ver_str = disk_ver.valid() ? disk_ver.to_string() : ""; + if (disk_ver_str != cached_ver.get()) return false; } @@ -1342,24 +1376,39 @@ bool GuideFrame::try_load_guide_cache() } } -// Builds guide profile JSON from the bundled system preset cache -// (resources/profiles/system_presets_cache.cache, generated by CI from all vendor JSONs). +// Builds guide profile JSON from the per-vendor bundled caches +// (resources/profiles/.cache, generated by CI). // This avoids the 90-second LoadProfileFamily fallback on first launch. bool GuideFrame::BuildProfileDataFromBundledCache() { - PresetBundleCache::SystemPresetsCache cache; - if (!cache.load(PresetBundleCache::SystemPresetsCache::bundled_cache_path())) + // Enumerate per-vendor .cache files in resources/profiles/. + std::vector cache_files; + try { + for (const auto& e : boost::filesystem::directory_iterator(rsrc_vendor_dir)) { + if (e.path().extension() == ".cache") + cache_files.push_back(e.path()); + } + } catch (const std::exception& ex) { + BOOST_LOG_TRIVIAL(warning) << "GuideFrame::BuildProfileDataFromBundledCache: cannot scan " << rsrc_vendor_dir << ": " << ex.what(); return false; - if (!cache.is_plausible()) - return false; - // Validate version strings against the current resources/profiles/ directory. - if (!cache.is_valid(rsrc_vendor_dir.string())) + } + if (cache_files.empty()) return false; try { - // Models from cached vendor profiles - for (const auto& cvp : cache.vendor_profiles) { - for (const auto& cm : cvp.models) { + for (const auto& cache_path : cache_files) { + const std::string vendor_id = cache_path.stem().string(); + // Validate against the version in the corresponding vendor JSON. + const boost::filesystem::path json_path = rsrc_vendor_dir / (vendor_id + ".json"); + const Semver ver = get_version_from_json(json_path.string()); + const std::string ver_str = ver.valid() ? ver.to_string() : ""; + + VendorCache vc; + if (!vc.load(cache_path.string()) || !vc.is_valid(ver_str)) + continue; + + // Models from this vendor's cached profile + for (const auto& cm : vc.profile.models) { std::string nozzle_str; for (const auto& v : cm.variants) { if (!nozzle_str.empty()) nozzle_str += ";"; @@ -1371,7 +1420,7 @@ bool GuideFrame::BuildProfileDataFromBundledCache() materials_str += m; } boost::filesystem::path cover_path = - (boost::filesystem::path(resources_dir()) / "profiles" / cvp.id / (cm.id + "_cover.png")) + (boost::filesystem::path(resources_dir()) / "profiles" / vc.profile.id / (cm.id + "_cover.png")) .make_preferred(); if (!boost::filesystem::exists(cover_path)) cover_path = @@ -1381,7 +1430,7 @@ bool GuideFrame::BuildProfileDataFromBundledCache() json entry; entry["model"] = cm.id; entry["name"] = cm.name; - entry["vendor"] = cvp.id; + entry["vendor"] = vc.profile.id; entry["nozzle_diameter"] = nozzle_str; entry["materials"] = materials_str; entry["cover"] = cover_path.string(); @@ -1389,60 +1438,60 @@ bool GuideFrame::BuildProfileDataFromBundledCache() 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("printer_model"); - const auto* pv = cp.config.option("printer_variant"); - if (!pm || pm->value.empty() || !pv) continue; + // Machines from cached printer presets + for (const auto& cp : vc.printer_presets) { + const auto* pm = cp.config.option("printer_model"); + const auto* pv = cp.config.option("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("filament_vendor"); - const auto* ft = cp.config.option("filament_type"); - const auto* compat = cp.config.option("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 mach; + mach["model"] = pm->value; + mach["nozzle"] = pv->value; + m_ProfileJson["machine"][cp.name] = mach; } - 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; + // Filaments from cached filament presets + for (const auto& cp : vc.filament_presets) { + const auto* fv = cp.config.option("filament_vendor"); + const auto* ft = cp.config.option("filament_type"); + const auto* compat = cp.config.option("compatible_printers"); + + std::string vendor = (fv && !fv->values.empty()) ? fv->values[0] : ""; + std::string type = (ft && !ft->values.empty()) ? ft->values[0] : ""; + + std::string model_list; + if (compat) { + for (const std::string& pname : compat->values) { + if (m_ProfileJson["machine"].contains(pname)) { + std::string m = m_ProfileJson["machine"][pname]["model"]; + std::string n = m_ProfileJson["machine"][pname]["nozzle"]; + model_list += "[" + m + "++" + n + "]"; + } + } + } + + json ff; + ff["name"] = cp.name; + ff["sub_path"] = cp.file; + ff["vendor"] = vendor; + ff["type"] = type; + ff["models"] = model_list; + ff["selected"] = 0; + m_ProfileJson["filament"][cp.name] = ff; + } + + // Process from cached print presets + for (const auto& cp : vc.print_presets) { + if (!cp.is_visible) continue; + json entry; + entry["name"] = cp.name; + entry["sub_path"] = cp.file; + m_ProfileJson["process"].push_back(entry); + } } - // 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 (" + BOOST_LOG_TRIVIAL(info) << "GuideFrame: built profile data from bundled per-vendor caches (" << m_ProfileJson["model"].size() << " models, " << m_ProfileJson["machine"].size() << " machines, " << m_ProfileJson["filament"].size() << " filaments)"; @@ -1468,8 +1517,9 @@ void GuideFrame::save_guide_cache() const 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. + const Semver ver = get_version_from_json(e.path().string()); cache["vendor_versions"][e.path().filename().string()] = - read_vendor_json_version(e.path()); + ver.valid() ? ver.to_string() : ""; } } cache["profile_data"] = m_ProfileJson; @@ -1486,113 +1536,62 @@ void GuideFrame::save_guide_cache() const int GuideFrame::LoadProfileData() { + // Background thread: Steps 1+2 already ran on the main thread in OnNavigationComplete + // and both failed. This function handles only Steps 3 (bundled cache) and 4 (slow JSON walk). try { - m_ProfileJson = json::parse("{}"); - m_ProfileJson["model"] = json::array(); - m_ProfileJson["machine"] = json::object(); - m_ProfileJson["filament"] = json::object(); - m_ProfileJson["process"] = json::array(); + // 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 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); - vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / PRESET_SYSTEM_DIR).make_preferred(); - rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - - // Orca: add custom as default - // Orca: add json logic for vendor bundle - orca_bundle_rsrc = true; - - // search if there exists a .json file in vendor_dir folder, if exists, set orca_bundle_rsrc to false - for (const auto& entry : boost::filesystem::directory_iterator(vendor_dir)) { - if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json") && !boost::iequals(entry.path().stem().string(), PresetBundle::ORCA_FILAMENT_LIBRARY)) { - orca_bundle_rsrc = false; - break; - } - } - - // 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(); - } - - // 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 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_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(); + 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; } } + // Promote to user guide cache so every subsequent open takes ~50ms. + if (!m_destroy) + save_guide_cache(); + wxGetApp().CallAfter([this] { - if (!m_destroy) { - //sync to appconfig first to populate current selections - SaveProfileData(); - - //sync to web after selections are populated - std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore); - - BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents: " << std::endl << strAll; - json m_Res = json::object(); - m_Res["command"] = "userguide_profile_load_finish"; - m_Res["sequence_id"] = "10001"; - wxString strJS = wxString::Format("HandleStudio(%s)", m_Res.dump(-1, ' ', true)); - - RunScript(strJS); - } + if (!m_destroy) + on_profile_loaded(); }); - } catch (std::exception& e) { - // wxLogMessage("GUIDE: load_profile_error %s ", e.what()); - // wxMessageBox(e.what(), "", MB_OK); - BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what() << std::endl; + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ", error: " << e.what(); } filament_info_cache.clear(); diff --git a/src/slic3r/GUI/WebGuideDialog.hpp b/src/slic3r/GUI/WebGuideDialog.hpp index eaa8c83d74..bc23b8b2b2 100644 --- a/src/slic3r/GUI/WebGuideDialog.hpp +++ b/src/slic3r/GUI/WebGuideDialog.hpp @@ -30,6 +30,7 @@ #include "libslic3r/PresetBundle.hpp" #include "slic3r/Utils/PresetUpdater.hpp" +#include #include #include @@ -78,6 +79,8 @@ public: int LoadProfileData(); int SaveProfileData(); int LoadProfileFamily(std::string strVendor, std::string strFilePath); + void init_guide_paths(); + void on_profile_loaded(); bool BuildProfileDataFromPresetBundle(); bool BuildProfileDataFromBundledCache(); bool try_load_guide_cache(); @@ -116,7 +119,7 @@ private: //First Load bool bFirstComplete{false}; - bool m_destroy{false}; + std::atomic m_destroy{false}; boost::thread* m_load_task{ nullptr }; // User Config @@ -127,6 +130,7 @@ private: bool InstallNetplugin; bool network_plugin_ready {false}; + json m_ProfileJson; json m_OrcaFilaList; std::string m_OrcaFilaLibPath;