mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 18:01:14 +00:00
Pack bed texture and model into printer bundles (#15965)
* feat(preset): pack bed texture and model into printer bundles Export copies bed_custom_texture and bed_custom_model into the .orca_printer zip under bed/. The manifest lists them in printer_asset. Import extracts each file into the bundle folder and rewrites the preset to that path. A missing file is skipped. A path that leaves the bundle directory is rejected. An old bundle with no printer_asset imports as before. * Add includes flagged by clang-tidy misc-include-cleaner * Gate bed asset packing on an export checkbox and key printer_asset per printer The Export Configs checkbox can leave a custom bed out of a shared printer bundle. printer_asset is keyed by each printer_config zip entry, with files under bed/<n>/, so a later multi-printer bundle does not need a second manifest. The unshipped flat printer_asset object is not read. --------- Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
co-authored by
Rodrigo Faselli
parent
210bc927bc
commit
295094a594
@@ -405,6 +405,7 @@ set(lisbslic3r_sources
|
||||
Polyline.hpp
|
||||
PresetBundle.cpp
|
||||
PresetBundle.hpp
|
||||
PrinterBedAssets.hpp
|
||||
PresetCacheFormat.cpp
|
||||
PresetCacheFormat.hpp
|
||||
Preset.cpp
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <cstdint>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <ios>
|
||||
#include <optional>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
@@ -29,6 +30,7 @@
|
||||
#include <sstream>
|
||||
|
||||
#include "PresetBundle.hpp"
|
||||
#include "PrinterBedAssets.hpp"
|
||||
|
||||
#include "Config.hpp"
|
||||
#include "Exception.hpp"
|
||||
@@ -64,6 +66,7 @@
|
||||
#include <boost/algorithm/clamp.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
@@ -1678,6 +1681,258 @@ bool PresetBundle::apply_vendor_config(
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The two bed files a printer bundle can carry. Zip names are bed/<n>/<stem><ext>, never the source basename.
|
||||
struct BedAssetSlot {
|
||||
const char *option;
|
||||
const char *stem;
|
||||
};
|
||||
|
||||
constexpr BedAssetSlot k_bed_asset_slots[] = {
|
||||
{"bed_custom_texture", "texture"},
|
||||
{"bed_custom_model", "model"},
|
||||
};
|
||||
|
||||
std::string zip_slashes(std::string name)
|
||||
{
|
||||
for (char &c : name)
|
||||
if (c == '\\')
|
||||
c = '/';
|
||||
return name;
|
||||
}
|
||||
|
||||
// The bed dialog loads only png/svg textures and stl models. Anything else is not a bed asset.
|
||||
bool bed_extension_allowed(const char *option, const std::string &ext)
|
||||
{
|
||||
if (std::string(option) == "bed_custom_texture")
|
||||
return boost::iequals(ext, ".png") || boost::iequals(ext, ".svg");
|
||||
if (std::string(option) == "bed_custom_model")
|
||||
return boost::iequals(ext, ".stl");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extension including the dot, or empty when this option may not use it.
|
||||
std::string bed_asset_extension(const std::string &path, const char *option)
|
||||
{
|
||||
const auto slash = path.find_last_of("/\\");
|
||||
const auto base = slash == std::string::npos ? 0 : slash + 1;
|
||||
const auto dot = path.find_last_of('.');
|
||||
if (dot == std::string::npos || dot < base || dot + 1 >= path.size())
|
||||
return {};
|
||||
const std::string ext = path.substr(dot);
|
||||
if (!bed_extension_allowed(option, ext))
|
||||
return {};
|
||||
return ext;
|
||||
}
|
||||
|
||||
bool utf8_is_file(const std::string &path)
|
||||
{
|
||||
if (path.empty())
|
||||
return false;
|
||||
boost::system::error_code ec;
|
||||
#ifdef _WIN32
|
||||
// A narrow path is the ANSI code page unless nowide_filesystem() has run.
|
||||
const std::wstring wide = boost::nowide::widen(path);
|
||||
if (wide.empty())
|
||||
return false;
|
||||
return boost::filesystem::is_regular_file(boost::filesystem::path(wide), ec) && !ec;
|
||||
#else
|
||||
return boost::filesystem::is_regular_file(path, ec) && !ec;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string bed_asset_dest_utf8(const fs::path &dest)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return boost::nowide::narrow(dest.wstring());
|
||||
#else
|
||||
return dest.string();
|
||||
#endif
|
||||
}
|
||||
|
||||
// True only for bed/<digits>/<stem><ext> with an extension the bed dialog accepts for this field.
|
||||
bool accepted_bed_zip_name(const std::string &raw, const BedAssetSlot &slot, const fs::path &bundle_dir,
|
||||
std::string &zip_name, std::string &folder)
|
||||
{
|
||||
const std::string name = zip_slashes(raw);
|
||||
if (name.find("..") != std::string::npos)
|
||||
return false;
|
||||
if (name.size() < 8 || name.compare(0, 4, "bed/") != 0)
|
||||
return false;
|
||||
const auto slash = name.find('/', 4);
|
||||
if (slash == std::string::npos || slash == 4 || name.find('/', slash + 1) != std::string::npos)
|
||||
return false;
|
||||
const std::string digits = name.substr(4, slash - 4);
|
||||
for (unsigned char c : digits) {
|
||||
if (!std::isdigit(c))
|
||||
return false;
|
||||
}
|
||||
const std::string filename = name.substr(slash + 1);
|
||||
const std::string ext = bed_asset_extension(filename, slot.option);
|
||||
if (ext.empty() || filename != std::string(slot.stem) + ext)
|
||||
return false;
|
||||
if (!is_path_within_root(name, bundle_dir))
|
||||
return false;
|
||||
zip_name = name;
|
||||
folder = digits;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct PendingBedAsset {
|
||||
std::string option;
|
||||
std::string zip_name;
|
||||
std::string dest;
|
||||
std::string printer_key;
|
||||
fs::path final_path;
|
||||
fs::path staged;
|
||||
};
|
||||
|
||||
// Validated asset names only. A raw manifest value that fails the check is not skipped later,
|
||||
// so a value like printer/Name.json cannot hide that preset.
|
||||
// printer_asset is keyed by printer_config zip entry. A flat (unkeyed) object is not read.
|
||||
std::vector<PendingBedAsset> pending_bed_assets(const json &bundle_json, const fs::path &bundle_dir,
|
||||
std::unordered_set<std::string> &skip_names,
|
||||
const std::unordered_set<std::string> &zip_entries)
|
||||
{
|
||||
std::vector<PendingBedAsset> pending;
|
||||
if (!bundle_json.contains("printer_asset") || !bundle_json["printer_asset"].is_object())
|
||||
return pending;
|
||||
const json &assets = bundle_json["printer_asset"];
|
||||
for (auto it = assets.begin(); it != assets.end(); ++it) {
|
||||
const std::string printer_key = it.key();
|
||||
if (zip_entries.count(printer_key) == 0) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset entry is ignored, no matching printer in the zip: " << printer_key;
|
||||
continue;
|
||||
}
|
||||
if (!it.value().is_object())
|
||||
continue;
|
||||
const json &per_printer = it.value();
|
||||
for (const BedAssetSlot &slot : k_bed_asset_slots) {
|
||||
if (!per_printer.contains(slot.option) || !per_printer[slot.option].is_string())
|
||||
continue;
|
||||
const std::string raw = per_printer[slot.option].get<std::string>();
|
||||
std::string zip_name;
|
||||
std::string folder;
|
||||
if (!accepted_bed_zip_name(raw, slot, bundle_dir, zip_name, folder)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset path is rejected: " << raw;
|
||||
continue;
|
||||
}
|
||||
const std::string filename = zip_name.substr(zip_name.rfind('/') + 1);
|
||||
const fs::path final_path = fs::absolute(bundle_dir / "bed" / folder / filename);
|
||||
pending.push_back({slot.option, zip_name, bed_asset_dest_utf8(final_path), printer_key, final_path, {}});
|
||||
skip_names.insert(zip_name);
|
||||
}
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
// Stage under temp_folder. The bundle bed/ directory is not touched until a printer preset keeps the path.
|
||||
bool stage_bed_asset(mz_zip_archive &zip, PendingBedAsset &asset, const fs::path &temp_folder)
|
||||
{
|
||||
size_t size = 0;
|
||||
void *buf = mz_zip_reader_extract_file_to_heap(&zip, asset.zip_name.c_str(), &size, MZ_ZIP_FLAG_CASE_SENSITIVE);
|
||||
if (buf == nullptr) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset is missing, skipping " << asset.option << ": " << asset.zip_name;
|
||||
return false;
|
||||
}
|
||||
const std::string filename = asset.zip_name.substr(asset.zip_name.rfind('/') + 1);
|
||||
asset.staged = temp_folder / "bed" / asset.final_path.parent_path().filename() / filename;
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(asset.staged.parent_path(), ec);
|
||||
const std::string staged_utf8 = bed_asset_dest_utf8(asset.staged);
|
||||
bool wrote = false;
|
||||
if (!ec) {
|
||||
boost::nowide::ofstream out(staged_utf8, std::ios::binary | std::ios::trunc);
|
||||
wrote = static_cast<bool>(out);
|
||||
if (wrote && size > 0)
|
||||
wrote = static_cast<bool>(out.write(static_cast<const char *>(buf), static_cast<std::streamsize>(size)));
|
||||
}
|
||||
mz_free(buf);
|
||||
if (!wrote) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset could not be written, skipping " << asset.option << ": " << staged_utf8;
|
||||
fs::remove(asset.staged, ec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool install_bed_asset(const PendingBedAsset &asset)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
fs::create_directories(asset.final_path.parent_path(), ec);
|
||||
if (!ec)
|
||||
fs::copy_file(asset.staged, asset.final_path, fs::copy_options::overwrite_existing, ec);
|
||||
if (ec) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Printer bundle asset could not be installed: " << asset.dest;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_pending_bed_asset_entry(const std::string &entry, const std::unordered_set<std::string> &names)
|
||||
{
|
||||
return names.count(entry) != 0 || names.count(zip_slashes(entry)) != 0;
|
||||
}
|
||||
|
||||
bool printer_kept_bed_paths(const PresetCollection &printers, const std::map<std::string, std::string> &paths)
|
||||
{
|
||||
if (paths.empty())
|
||||
return false;
|
||||
for (const Preset &preset : printers.get_presets()) {
|
||||
for (const auto &entry : paths)
|
||||
if (preset.config.opt_string(entry.first) == entry.second)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool append_printer_bed_assets(mz_zip_archive &zip, const DynamicPrintConfig &config, json &bundle_structure,
|
||||
const std::string &printer_config_entry, int printer_index)
|
||||
{
|
||||
if (printer_config_entry.empty() || printer_index < 0)
|
||||
return true;
|
||||
json assets = json::object();
|
||||
for (const BedAssetSlot &slot : k_bed_asset_slots) {
|
||||
if (!config.has(slot.option))
|
||||
continue;
|
||||
const std::string stored = config.opt_string(slot.option);
|
||||
if (stored.empty())
|
||||
continue;
|
||||
if (!utf8_is_file(stored)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Bed asset file is missing, skipping " << slot.option << ": " << stored;
|
||||
continue;
|
||||
}
|
||||
const std::string ext = bed_asset_extension(stored, slot.option);
|
||||
if (ext.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Bed asset extension is not packable, skipping " << slot.option << ": " << stored;
|
||||
continue;
|
||||
}
|
||||
boost::nowide::ifstream in(stored, std::ios::binary);
|
||||
if (!in) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Bed asset file cannot be read, skipping " << slot.option << ": " << stored;
|
||||
continue;
|
||||
}
|
||||
const std::string bytes((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
if (in.bad()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Bed asset file cannot be read, skipping " << slot.option << ": " << stored;
|
||||
continue;
|
||||
}
|
||||
const std::string zip_name = std::string("bed/") + std::to_string(printer_index) + "/" + slot.stem + ext;
|
||||
const void *data = bytes.empty() ? static_cast<const void *>("") : static_cast<const void *>(bytes.data());
|
||||
if (mz_zip_writer_add_mem(&zip, zip_name.c_str(), data, bytes.size(), MZ_DEFAULT_COMPRESSION) == MZ_FALSE) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to add bed asset to printer bundle: " << zip_name;
|
||||
return false;
|
||||
}
|
||||
assets[slot.option] = zip_name;
|
||||
}
|
||||
if (!assets.empty())
|
||||
bundle_structure["printer_asset"][printer_config_entry] = std::move(assets);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Import presets from UI control
|
||||
PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string> & files,
|
||||
std::function<int(std::string const &)> override_confirm,
|
||||
@@ -1773,14 +2028,44 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " No bundle_structure.json found, importing presets into the user preset directory";
|
||||
}
|
||||
|
||||
json bundle_json = json::object();
|
||||
std::unordered_set<std::string> bed_asset_entries;
|
||||
std::vector<PendingBedAsset> bed_assets;
|
||||
std::map<std::string, std::map<std::string, std::string>> bed_paths_by_printer;
|
||||
bool bed_paths_loaded = false;
|
||||
if (has_bundle_structure && !bundle_base_dir.empty()) {
|
||||
try {
|
||||
boost::nowide::ifstream manifest_in(metadata_path.string());
|
||||
manifest_in >> bundle_json;
|
||||
} catch (const std::exception &err) {
|
||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " failed to read bundle manifest: " << err.what();
|
||||
bundle_json = json::object();
|
||||
}
|
||||
std::unordered_set<std::string> zip_entries;
|
||||
const int zip_file_count = mz_zip_reader_get_num_files(&zip_archive);
|
||||
for (int zi = 0; zi < zip_file_count; ++zi) {
|
||||
mz_zip_archive_file_stat st;
|
||||
if (mz_zip_reader_file_stat(&zip_archive, zi, &st))
|
||||
zip_entries.insert(zip_slashes(st.m_filename));
|
||||
}
|
||||
bed_assets = pending_bed_assets(bundle_json, bundle_base_dir, bed_asset_entries, zip_entries);
|
||||
for (PendingBedAsset &asset : bed_assets)
|
||||
if (stage_bed_asset(zip_archive, asset, temp_folder))
|
||||
bed_paths_by_printer[asset.printer_key].emplace(asset.option, asset.dest);
|
||||
}
|
||||
|
||||
// Extract Files
|
||||
int num_files = mz_zip_reader_get_num_files(&zip_archive);
|
||||
const std::map<std::string, std::string> no_bed_paths;
|
||||
for (int i = 0; i < num_files; i++) {
|
||||
mz_zip_archive_file_stat file_stat;
|
||||
status = mz_zip_reader_file_stat(&zip_archive, i, &file_stat);
|
||||
if (status) {
|
||||
std::string file_name = file_stat.m_filename;
|
||||
const std::string zip_entry = zip_slashes(file_name);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " From zip file: " << file << ". Read file name: " << file_stat.m_filename;
|
||||
if (is_pending_bed_asset_entry(file_name, bed_asset_entries))
|
||||
continue;
|
||||
size_t index = file_name.find_last_of("/\\");
|
||||
if (std::string::npos != index) {
|
||||
file_name = file_name.substr(index + 1);
|
||||
@@ -1798,13 +2083,26 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
|
||||
if (MZ_FALSE == status) {
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to open target file: " << target_file_path;
|
||||
} else {
|
||||
const auto paths_it = bed_paths_by_printer.find(zip_entry);
|
||||
const std::map<std::string, std::string> &this_bed_paths =
|
||||
paths_it != bed_paths_by_printer.end() ? paths_it->second : no_bed_paths;
|
||||
bool is_success = import_json_presets(substitutions, target_file_path, override_confirm, rule, overwrite, result,
|
||||
has_bundle_structure ? bundle_base_dir.string() : std::string());
|
||||
has_bundle_structure ? bundle_base_dir.string() : std::string(),
|
||||
this_bed_paths, &bed_paths_loaded);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " import target file: " << target_file_path << " import result" << is_success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy into bed/ only after this import loads a printer that still holds the staged path.
|
||||
// A preset left from an earlier import must not count: declining this import leaves its files alone.
|
||||
if (bed_paths_loaded) {
|
||||
for (const PendingBedAsset &asset : bed_assets) {
|
||||
if (printer_kept_bed_paths(printers, {{asset.option, asset.dest}}))
|
||||
install_bed_asset(asset);
|
||||
}
|
||||
}
|
||||
|
||||
// Set imported_time to current time if not already set
|
||||
if (metadata.imported_time == 0) {
|
||||
metadata.imported_time = std::time(nullptr);
|
||||
@@ -1846,7 +2144,9 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
|
||||
ForwardCompatibilitySubstitutionRule rule,
|
||||
int & overwrite,
|
||||
std::vector<std::string> & result,
|
||||
const std::string & bundle_dir)
|
||||
const std::string & bundle_dir,
|
||||
const std::map<std::string, std::string> & bed_asset_paths,
|
||||
bool * bed_paths_loaded)
|
||||
{
|
||||
try {
|
||||
DynamicPrintConfig config;
|
||||
@@ -1910,6 +2210,10 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
|
||||
if (inherit_preset)
|
||||
inherits_value = inherit_preset->name; // keep the base_id redo below in sync
|
||||
}
|
||||
if (collection == &printers) {
|
||||
for (const auto &entry : bed_asset_paths)
|
||||
config.option<ConfigOptionString>(entry.first, true)->value = entry.second;
|
||||
}
|
||||
if (inherit_preset) {
|
||||
new_config = inherit_preset->config;
|
||||
new_config.apply(std::move(config));
|
||||
@@ -1927,6 +2231,8 @@ bool PresetBundle::import_json_presets(PresetsConfigSubstitutions & s
|
||||
new_config.apply(std::move(config));
|
||||
extend_default_config_length(new_config, true, default_preset.config);
|
||||
}
|
||||
if (bed_paths_loaded != nullptr && collection == &printers && !bed_asset_paths.empty())
|
||||
*bed_paths_loaded = true;
|
||||
|
||||
Preset &preset = collection->load_preset(collection->path_from_name(name, inherit_preset == nullptr), preset_name, std::move(new_config), false);
|
||||
preset.bundle_id = load_origin.bundle_id;
|
||||
|
||||
@@ -319,7 +319,9 @@ public:
|
||||
ForwardCompatibilitySubstitutionRule rule,
|
||||
int& overwrite,
|
||||
std::vector<std::string>& result,
|
||||
const std::string& bundle_dir = "");
|
||||
const std::string& bundle_dir = "",
|
||||
const std::map<std::string, std::string> &bed_asset_paths = {},
|
||||
bool *bed_paths_loaded = nullptr);
|
||||
|
||||
void save_user_presets(AppConfig& config, std::map<std::string, std::string>& need_to_delete_list);
|
||||
void check_and_fix_user_presets_syncinfo(const std::string& user_id);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "miniz_extension.hpp"
|
||||
|
||||
#include <miniz.h>
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class DynamicPrintConfig;
|
||||
|
||||
// Not on PresetBundle.hpp: miniz.h's zlib-compat macros rename GUI methods (GLTexture::Compressor::compress).
|
||||
// printer_config_entry is the zip name also pushed onto bundle_structure["printer_config"].
|
||||
// printer_index is that entry's 0-based index in the exported printer_config list.
|
||||
bool append_printer_bed_assets(mz_zip_archive &zip, const DynamicPrintConfig &config, nlohmann::json &bundle_structure,
|
||||
const std::string &printer_config_entry, int printer_index);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -65,6 +65,7 @@
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <wx/valtext.h>
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/PrinterBedAssets.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
@@ -3674,9 +3675,20 @@ ExportConfigsDialog::~ExportConfigsDialog()
|
||||
}
|
||||
|
||||
void ExportConfigsDialog::on_dpi_changed(const wxRect &suggested_rect) {
|
||||
if (m_include_bed_assets)
|
||||
m_include_bed_assets->Rescale();
|
||||
Layout();
|
||||
}
|
||||
|
||||
void ExportConfigsDialog::update_include_bed_assets_enabled()
|
||||
{
|
||||
const bool on = get_curr_radio_type(m_export_type_btns) == m_exprot_type.preset_bundle;
|
||||
if (m_include_bed_assets)
|
||||
m_include_bed_assets->Enable(on);
|
||||
if (m_include_bed_assets_label)
|
||||
m_include_bed_assets_label->Enable(on);
|
||||
}
|
||||
|
||||
void ExportConfigsDialog::show_export_result(const ExportCase &export_case)
|
||||
{
|
||||
MessageDialog *msg_dlg = nullptr;
|
||||
@@ -3819,6 +3831,17 @@ wxBoxSizer *ExportConfigsDialog::create_export_config_item(wxWindow *parent)
|
||||
static_export_printer_preset_bundle_text->SetFont(Label::Body_12);
|
||||
static_export_printer_preset_bundle_text->SetForegroundColour(wxColour("#6B6B6B"));
|
||||
radioBoxSizer->Add(static_export_printer_preset_bundle_text, 0, wxEXPAND | wxLEFT, FromDIP(22));
|
||||
wxBoxSizer *include_bed_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||
m_include_bed_assets = new ::CheckBox(parent);
|
||||
m_include_bed_assets->SetValue(true);
|
||||
include_bed_sizer->Add(m_include_bed_assets, 0, wxALIGN_CENTER_VERTICAL, 0);
|
||||
m_include_bed_assets_label = new wxStaticText(parent, wxID_ANY, _L("Include custom bed texture and model"));
|
||||
m_include_bed_assets_label->SetFont(Label::Body_12);
|
||||
m_include_bed_assets_label->SetForegroundColour(wxColour("#6B6B6B"));
|
||||
include_bed_sizer->Add(m_include_bed_assets_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
|
||||
radioBoxSizer->Add(0, 0, 0, wxTOP, FromDIP(6));
|
||||
radioBoxSizer->Add(include_bed_sizer, 0, wxEXPAND | wxLEFT, FromDIP(22));
|
||||
update_include_bed_assets_enabled();
|
||||
radioBoxSizer->Add(create_radio_item(m_exprot_type.filament_bundle, parent, wxEmptyString, m_export_type_btns), 0, wxEXPAND | wxTOP, FromDIP(10));
|
||||
wxStaticText *static_export_filament_preset_bundle_text = new wxStaticText(parent, wxID_ANY, _L("User's filament preset set.\nCan be shared with others."),
|
||||
wxDefaultPosition, wxDefaultSize);
|
||||
@@ -3988,9 +4011,10 @@ void ExportConfigsDialog::select_curr_radiobox(std::vector<std::pair<RadioBox *,
|
||||
radiobox_list[i].first->SetValue(false);
|
||||
}
|
||||
}
|
||||
update_include_bed_assets_enabled();
|
||||
}
|
||||
|
||||
ExportConfigsDialog::ExportCase ExportConfigsDialog::archive_preset_bundle_to_file(const wxString &path)
|
||||
ExportConfigsDialog::ExportCase ExportConfigsDialog::archive_preset_bundle_to_file(const wxString &path, bool include_bed_assets)
|
||||
{
|
||||
std::string export_path = initial_file_path(path, "");
|
||||
if (export_path.empty() || "initial_failed" == export_path) return ExportCase::EXPORT_CANCEL;
|
||||
@@ -4092,6 +4116,12 @@ ExportConfigsDialog::ExportCase ExportConfigsDialog::archive_preset_bundle_to_fi
|
||||
bundle_structure["filament_config"] = filament_configs;
|
||||
bundle_structure["process_config"] = process_configs;
|
||||
|
||||
if (include_bed_assets &&
|
||||
!append_printer_bed_assets(zip_archive, printer_preset->config, bundle_structure, printer_config_file_name, 0)) {
|
||||
mz_zip_writer_end(&zip_archive);
|
||||
return ExportCase::ADD_FILE_FAIL;
|
||||
}
|
||||
|
||||
std::string bundle_structure_str = bundle_structure.dump();
|
||||
status = mz_zip_writer_add_mem(&zip_archive, BUNDLE_STRUCTURE_JSON_NAME, bundle_structure_str.data(), bundle_structure_str.size(), MZ_DEFAULT_COMPRESSION);
|
||||
if (MZ_FALSE == status) {
|
||||
@@ -4339,7 +4369,7 @@ wxWindow *ExportConfigsDialog::create_dialog_buttons(wxWindow* parent)
|
||||
const wxString curr_radio_type = get_curr_radio_type(m_export_type_btns);
|
||||
|
||||
if (curr_radio_type == m_exprot_type.preset_bundle) {
|
||||
export_case = archive_preset_bundle_to_file(path);
|
||||
export_case = archive_preset_bundle_to_file(path, m_include_bed_assets->GetValue());
|
||||
} else if (curr_radio_type == m_exprot_type.filament_bundle) {
|
||||
export_case = archive_filament_bundle_to_file(path);
|
||||
} else if (curr_radio_type == m_exprot_type.printer_preset) {
|
||||
|
||||
@@ -271,6 +271,7 @@ protected:
|
||||
private:
|
||||
void data_init();
|
||||
void select_curr_radiobox(std::vector<std::pair<RadioBox *, wxString>> &radiobox_list, int btn_idx);
|
||||
void update_include_bed_assets_enabled();
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||
void show_export_result(const ExportCase &export_case);
|
||||
bool has_check_box_selected();
|
||||
@@ -284,7 +285,7 @@ private:
|
||||
int initial_zip_archive(mz_zip_archive &zip_archive, const std::string &file_path);
|
||||
ExportCase save_zip_archive_to_file(mz_zip_archive &zip_archive);
|
||||
ExportCase save_presets_to_zip(const std::string &export_file, const std::vector<std::pair<std::string, std::string>> &config_paths);
|
||||
ExportCase archive_preset_bundle_to_file(const wxString &path);
|
||||
ExportCase archive_preset_bundle_to_file(const wxString &path, bool include_bed_assets);
|
||||
ExportCase archive_filament_bundle_to_file(const wxString &path);
|
||||
ExportCase archive_printer_preset_to_file(const wxString &path);
|
||||
ExportCase archive_filament_preset_to_file(const wxString &path);
|
||||
@@ -304,6 +305,8 @@ private:
|
||||
wxGridSizer * m_preset_sizer = nullptr;
|
||||
wxPanel * m_presets_window = nullptr;
|
||||
wxStaticText * m_serial_text = nullptr;
|
||||
::CheckBox * m_include_bed_assets = nullptr;
|
||||
wxStaticText * m_include_bed_assets_label = nullptr;
|
||||
};
|
||||
|
||||
class CreatePresetForPrinterDialog : public DPIDialog
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
|
||||
@@ -22,6 +24,7 @@
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/miniz_extension.hpp"
|
||||
#include "libslic3r/PrinterBedAssets.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
@@ -5853,6 +5856,609 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const std::string kTextureBytes("\x89PNG\x00tex", 8);
|
||||
const std::string kModelBytes("stl\x00model", 9);
|
||||
const std::string kOtherBytes("\x89PNG\x00other", 10);
|
||||
|
||||
void write_bytes(const std::string &utf8_path, const std::string &bytes)
|
||||
{
|
||||
const auto slash = utf8_path.find_last_of("/\\");
|
||||
if (slash != std::string::npos) {
|
||||
#ifdef _WIN32
|
||||
fs::create_directories(fs::path(boost::nowide::widen(utf8_path.substr(0, slash))));
|
||||
#else
|
||||
fs::create_directories(utf8_path.substr(0, slash));
|
||||
#endif
|
||||
}
|
||||
boost::nowide::ofstream out(utf8_path, std::ios::binary | std::ios::trunc);
|
||||
REQUIRE(out);
|
||||
out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
|
||||
REQUIRE(out);
|
||||
}
|
||||
|
||||
fs::path write_printer_json(const DynamicPrintConfig &defaults, const fs::path &file, const std::string &name,
|
||||
const std::string &texture, const std::string &model, const std::string &inherits = {})
|
||||
{
|
||||
DynamicPrintConfig config(defaults);
|
||||
config.option<ConfigOptionString>("printer_settings_id", true)->value = name;
|
||||
config.option<ConfigOptionString>("inherits", true)->value = inherits;
|
||||
if (texture.empty())
|
||||
config.erase("bed_custom_texture");
|
||||
else
|
||||
config.option<ConfigOptionString>("bed_custom_texture", true)->value = texture;
|
||||
if (model.empty())
|
||||
config.erase("bed_custom_model");
|
||||
else
|
||||
config.option<ConfigOptionString>("bed_custom_model", true)->value = model;
|
||||
fs::create_directories(file.parent_path());
|
||||
config.save_to_json(file.string(), name, "User", "1.0.0");
|
||||
return file;
|
||||
}
|
||||
|
||||
nlohmann::json bed_manifest(const std::string &bundle_id)
|
||||
{
|
||||
nlohmann::json structure = nlohmann::json::object();
|
||||
structure["id"] = bundle_id;
|
||||
structure["bundle_type"] = "printer config bundle";
|
||||
structure["printer_config"] = nlohmann::json::array({"printer/BedPrinter.json"});
|
||||
return structure;
|
||||
}
|
||||
|
||||
void set_printer_bed_asset(nlohmann::json &structure, const std::string &option, const std::string &zip_name,
|
||||
const std::string &printer_entry = "printer/BedPrinter.json")
|
||||
{
|
||||
structure["printer_asset"][printer_entry][option] = zip_name;
|
||||
}
|
||||
|
||||
nlohmann::json pack_bed_zip(const fs::path &zip_file, const fs::path &printer_json, const DynamicPrintConfig &defaults,
|
||||
const std::string &texture, const std::string &model, const std::string &bundle_id,
|
||||
bool include_bed_assets = true)
|
||||
{
|
||||
DynamicPrintConfig config(defaults);
|
||||
if (!texture.empty())
|
||||
config.option<ConfigOptionString>("bed_custom_texture", true)->value = texture;
|
||||
if (!model.empty())
|
||||
config.option<ConfigOptionString>("bed_custom_model", true)->value = model;
|
||||
nlohmann::json structure = bed_manifest(bundle_id);
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
const std::string printer_bytes = read_file(printer_json);
|
||||
REQUIRE(open_zip_writer(&zip, zip_file.string()));
|
||||
REQUIRE(mz_zip_writer_add_mem(&zip, "printer/BedPrinter.json", printer_bytes.data(), printer_bytes.size(),
|
||||
MZ_DEFAULT_COMPRESSION));
|
||||
if (include_bed_assets)
|
||||
REQUIRE(append_printer_bed_assets(zip, config, structure, "printer/BedPrinter.json", 0));
|
||||
const std::string manifest = structure.dump();
|
||||
REQUIRE(mz_zip_writer_add_mem(&zip, BUNDLE_STRUCTURE_JSON_NAME, manifest.data(), manifest.size(),
|
||||
MZ_DEFAULT_COMPRESSION));
|
||||
REQUIRE(mz_zip_writer_finalize_archive(&zip));
|
||||
REQUIRE(close_zip_writer(&zip));
|
||||
return structure;
|
||||
}
|
||||
|
||||
std::vector<std::string> zip_entry_names(const fs::path &zip_file)
|
||||
{
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
REQUIRE(open_zip_reader(&zip, zip_file.string()));
|
||||
std::vector<std::string> names;
|
||||
const int n = static_cast<int>(mz_zip_reader_get_num_files(&zip));
|
||||
for (int i = 0; i < n; ++i) {
|
||||
mz_zip_archive_file_stat st;
|
||||
REQUIRE(mz_zip_reader_file_stat(&zip, i, &st));
|
||||
names.emplace_back(st.m_filename);
|
||||
}
|
||||
REQUIRE(close_zip_reader(&zip));
|
||||
return names;
|
||||
}
|
||||
|
||||
const Preset *import_named_printer(PresetBundle &bundle, const fs::path &zip_file, const fs::path &data_root,
|
||||
const std::string &bare_name, int confirm = 1)
|
||||
{
|
||||
ScopedDataDir scoped(data_root);
|
||||
AppConfig config;
|
||||
std::vector<std::string> files{zip_file.string()};
|
||||
bundle.import_presets(files, [confirm](std::string const &) { return confirm; },
|
||||
ForwardCompatibilitySubstitutionRule::Disable, config);
|
||||
for (const Preset &preset : bundle.printers.get_presets())
|
||||
if (get_preset_bare_name(preset.name) == bare_name && !preset.file.empty())
|
||||
return &preset;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Preset *import_bed(PresetBundle &bundle, const fs::path &zip_file, const fs::path &data_root, int confirm = 1)
|
||||
{
|
||||
return import_named_printer(bundle, zip_file, data_root, "BedPrinter", confirm);
|
||||
}
|
||||
|
||||
struct BedRoots {
|
||||
ScopedTemporaryDir temp_dir;
|
||||
fs::path src;
|
||||
fs::path data_root;
|
||||
PresetBundle bundle;
|
||||
BedRoots()
|
||||
: src(temp_dir.path() / "src")
|
||||
, data_root(temp_dir.path() / "datadir")
|
||||
{}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Printer bundle round trip packs bed texture and model and rewrites both keys", "[Preset][Bundle]")
|
||||
{
|
||||
// tex_rel "*" builds a non-ASCII source path under the temp directory.
|
||||
const auto pack_import = [](const std::string &bundle_id, const std::string &tex_rel, const std::string &mod_rel,
|
||||
const std::string &tex_bytes, const std::string &mod_bytes, bool tex_exists,
|
||||
bool mod_exists, int imports) {
|
||||
ScopedTemporaryDir temp_dir;
|
||||
const fs::path src = temp_dir.path() / "src";
|
||||
const fs::path data_root = temp_dir.path() / "datadir";
|
||||
std::string texture = tex_rel.empty() ? std::string() : (src / tex_rel).make_preferred().string();
|
||||
std::string model = mod_rel.empty() ? std::string() : (src / mod_rel).make_preferred().string();
|
||||
if (tex_rel == "*") {
|
||||
const fs::path dir = fs::path(src.wstring()) / fs::path(boost::nowide::widen("\xE5\xBA\x8A"));
|
||||
texture = boost::nowide::narrow((dir / fs::path(boost::nowide::widen("\xE7\xBA\xB9\xE7\x90\x86.png"))).wstring());
|
||||
}
|
||||
if (tex_exists)
|
||||
write_bytes(texture, tex_bytes);
|
||||
if (mod_exists)
|
||||
write_bytes(model, mod_bytes);
|
||||
PresetBundle bundle;
|
||||
const fs::path printer_json = write_printer_json(bundle.printers.default_preset().config, src / "BedPrinter.json",
|
||||
"BedPrinter", texture, model);
|
||||
const fs::path zip_file = src / "bundle.orca_printer";
|
||||
const nlohmann::json structure = pack_bed_zip(zip_file, printer_json, bundle.printers.default_preset().config,
|
||||
texture, model, bundle_id);
|
||||
const Preset *saved = nullptr;
|
||||
for (int i = 0; i < imports; ++i)
|
||||
saved = import_bed(bundle, zip_file, data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
const std::string got[] = {saved->config.opt_string("bed_custom_texture"), saved->config.opt_string("bed_custom_model")};
|
||||
const std::string rel[] = {tex_rel, mod_rel};
|
||||
const std::string bytes[] = {tex_bytes, mod_bytes};
|
||||
const std::string path[] = {texture, model};
|
||||
const bool exists[] = {tex_exists, mod_exists};
|
||||
const char *option[] = {"bed_custom_texture", "bed_custom_model"};
|
||||
const char *stem[] = {"texture", "model"};
|
||||
for (int slot = 0; slot < 2; ++slot) {
|
||||
if (exists[slot]) {
|
||||
const std::string ext = fs::path(rel[slot] == "*" ? "x.png" : rel[slot]).extension().string();
|
||||
const std::string zip_name = std::string("bed/0/") + stem[slot] + ext;
|
||||
REQUIRE(structure["printer_asset"]["printer/BedPrinter.json"][option[slot]] == zip_name);
|
||||
REQUIRE(fs::path(got[slot]).filename().string() == std::string(stem[slot]) + fs::path(zip_name).extension().string());
|
||||
REQUIRE(fs::path(got[slot]).parent_path().filename() == "0");
|
||||
REQUIRE(fs::path(got[slot]).parent_path().parent_path().filename() == "bed");
|
||||
REQUIRE(got[slot].find(bundle_id) != std::string::npos);
|
||||
REQUIRE(read_file(got[slot]) == bytes[slot]);
|
||||
REQUIRE(got[slot] != path[slot]);
|
||||
} else if (!rel[slot].empty() && rel[slot] != "*") {
|
||||
REQUIRE_FALSE(structure.value("printer_asset", nlohmann::json::object())
|
||||
.value("printer/BedPrinter.json", nlohmann::json::object())
|
||||
.contains(option[slot]));
|
||||
REQUIRE(got[slot] == path[slot]);
|
||||
} else if (rel[slot].empty()) {
|
||||
REQUIRE(got[slot].empty());
|
||||
}
|
||||
}
|
||||
if (imports > 1) {
|
||||
int copies = 0;
|
||||
for (fs::recursive_directory_iterator it(data_root), end; it != end; ++it)
|
||||
if (it->path().filename() == "texture.png" || it->path().filename() == "model.stl")
|
||||
++copies;
|
||||
REQUIRE(copies == (tex_exists ? 1 : 0) + (mod_exists ? 1 : 0));
|
||||
}
|
||||
};
|
||||
|
||||
SECTION("both assets are packed from the preset config and rewritten on import") {
|
||||
pack_import("bed-roundtrip", "fixture/baseline_bed_texture.png", "fixture/baseline_bed_model.stl",
|
||||
kTextureBytes, kModelBytes, true, true, 1);
|
||||
}
|
||||
SECTION("texture only") {
|
||||
pack_import("texture-only", "only.png", "", kTextureBytes, "", true, false, 1);
|
||||
}
|
||||
SECTION("model only") {
|
||||
pack_import("model-only", "", "only.stl", "", kModelBytes, false, true, 1);
|
||||
}
|
||||
SECTION("texture and model share a source basename") {
|
||||
pack_import("shared-basename", "a/plate.png", "b/plate.stl", kTextureBytes, kOtherBytes, true, true, 1);
|
||||
}
|
||||
SECTION("importing the same bundle twice keeps one copy of the assets") {
|
||||
// First import installs kTextureBytes / kModelBytes. The second pack uses different bytes so
|
||||
// the Yes-overwrite path (copy_file overwrite_existing) is actually proven.
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "twice.png").make_preferred().string();
|
||||
const std::string model = (bed.src / "twice.stl").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
write_bytes(model, kModelBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config,
|
||||
bed.src / "BedPrinter.json", "BedPrinter", texture, model);
|
||||
const fs::path zip_file = bed.src / "bundle.orca_printer";
|
||||
pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config, texture, model, "reimport");
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
const std::string got_tex = saved->config.opt_string("bed_custom_texture");
|
||||
const std::string got_mod = saved->config.opt_string("bed_custom_model");
|
||||
REQUIRE(read_file(got_tex) == kTextureBytes);
|
||||
REQUIRE(read_file(got_mod) == kModelBytes);
|
||||
write_bytes(texture, kOtherBytes);
|
||||
write_bytes(model, std::string("stl\x00other", 9));
|
||||
pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config, texture, model, "reimport");
|
||||
saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == got_tex);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_model") == got_mod);
|
||||
REQUIRE(read_file(got_tex) == kOtherBytes);
|
||||
REQUIRE(read_file(got_mod) == std::string("stl\x00other", 9));
|
||||
}
|
||||
SECTION("a non-ASCII source path is packed") {
|
||||
pack_import("non-ascii", "*", "", kTextureBytes, "", true, false, 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("An old printer bundle without printer_asset imports unchanged", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "old.png").make_preferred().string();
|
||||
const std::string model = (bed.src / "old.stl").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
write_bytes(model, kModelBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", texture, model);
|
||||
nlohmann::json structure = bed_manifest("old-bundle");
|
||||
REQUIRE_FALSE(structure.contains("printer_asset"));
|
||||
const fs::path zip_file = bed.src / "old.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}});
|
||||
// A bed/ folder left by an earlier import into the same bundle folder must survive an asset-less import.
|
||||
const fs::path old_bed = bed.data_root / "user" / "default" / "_local" / "old-bundle" / "bed";
|
||||
write_bytes((old_bed / "texture.png").string(), kTextureBytes);
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_model") == model);
|
||||
CHECK(fs::exists(old_bed / "texture.png"));
|
||||
}
|
||||
|
||||
TEST_CASE("Printer bundle asset names that escape the bundle directory are rejected", "[Preset][Bundle]")
|
||||
{
|
||||
const char *bad_names[] = {"../../pwned.png", "bed/../../pwned.png", "..\\..\\pwned.png", "C:/Windows/pwned.png",
|
||||
"machine/texture.png", "/bed/texture.png"};
|
||||
const std::string marker = "PWNED-BED-ASSET-MARKER";
|
||||
for (const char *name : bad_names) {
|
||||
DYNAMIC_SECTION("rejects " << name) {
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "kept.png").make_preferred().string();
|
||||
const std::string model = (bed.src / "kept.stl").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
write_bytes(model, kModelBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config,
|
||||
bed.src / "BedPrinter.json", "BedPrinter", texture, model);
|
||||
nlohmann::json structure = bed_manifest("traverse");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", name);
|
||||
std::vector<std::pair<std::string, std::string>> entries = {
|
||||
{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}};
|
||||
if (name[0] != '/' && name[0] != '\\')
|
||||
entries.emplace_back(name, marker);
|
||||
const fs::path zip_file = bed.src / "bad.orca_printer";
|
||||
write_zip(zip_file, entries);
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_model") == model);
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, fs::path(name).filename().string()));
|
||||
if (fs::exists(bed.data_root))
|
||||
for (fs::recursive_directory_iterator it(bed.data_root), end; it != end; ++it)
|
||||
if (fs::is_regular_file(*it))
|
||||
CHECK(read_file(*it).find(marker) == std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A printer bundle asset that is missing is skipped", "[Preset][Bundle]")
|
||||
{
|
||||
SECTION("a manifest entry with no zip bytes leaves the original path") {
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "gone.png").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", texture, {});
|
||||
nlohmann::json structure = bed_manifest("missing-zip");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
|
||||
const fs::path zip_file = bed.src / "missing.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}});
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
|
||||
}
|
||||
|
||||
SECTION("a missing file at export is omitted and the export still succeeds") {
|
||||
BedRoots bed;
|
||||
const std::string missing = (bed.src / "missing.png").make_preferred().string();
|
||||
const std::string model = (bed.src / "present.stl").make_preferred().string();
|
||||
write_bytes(model, kModelBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", missing, model);
|
||||
const fs::path zip_file = bed.src / "partial.orca_printer";
|
||||
const nlohmann::json structure = pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config,
|
||||
missing, model, "missing-export");
|
||||
REQUIRE_FALSE(structure["printer_asset"]["printer/BedPrinter.json"].contains("bed_custom_texture"));
|
||||
REQUIRE(structure["printer_asset"]["printer/BedPrinter.json"]["bed_custom_model"] == "bed/0/model.stl");
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == missing);
|
||||
REQUIRE(read_file(saved->config.opt_string("bed_custom_model")) == kModelBytes);
|
||||
REQUIRE(fs::path(saved->config.opt_string("bed_custom_model")).filename() == "model.stl");
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("An inherited printer bundle rewrites bed paths taken from the parent", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "parent.png").make_preferred().string();
|
||||
const std::string model = (bed.src / "parent.stl").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
write_bytes(model, kModelBytes);
|
||||
DynamicPrintConfig parent(bed.bundle.printers.default_preset().config);
|
||||
parent.option<ConfigOptionString>("printer_settings_id", true)->value = "ParentPrinter";
|
||||
parent.option<ConfigOptionString>("inherits", true)->value.clear();
|
||||
parent.option<ConfigOptionString>("bed_custom_texture", true)->value = texture;
|
||||
parent.option<ConfigOptionString>("bed_custom_model", true)->value = model;
|
||||
bed.bundle.printers.load_preset((bed.src / "ParentPrinter.json").string(), "ParentPrinter", std::move(parent), false);
|
||||
const fs::path child_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", {}, {}, "ParentPrinter");
|
||||
const nlohmann::json written = nlohmann::json::parse(read_file(child_json));
|
||||
REQUIRE_FALSE(written.contains("bed_custom_texture"));
|
||||
REQUIRE_FALSE(written.contains("bed_custom_model"));
|
||||
nlohmann::json structure = bed_manifest("inherit-bed");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
|
||||
set_printer_bed_asset(structure, "bed_custom_model", "bed/0/model.stl");
|
||||
const fs::path zip_file = bed.src / "inherit.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(child_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
|
||||
{"bed/0/texture.png", kTextureBytes}, {"bed/0/model.stl", kModelBytes}});
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") != texture);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_model") != model);
|
||||
REQUIRE(read_file(saved->config.opt_string("bed_custom_texture")) == kTextureBytes);
|
||||
REQUIRE(read_file(saved->config.opt_string("bed_custom_model")) == kModelBytes);
|
||||
REQUIRE(fs::path(saved->config.opt_string("bed_custom_texture")).filename() == "texture.png");
|
||||
REQUIRE(fs::path(saved->config.opt_string("bed_custom_model")).filename() == "model.stl");
|
||||
}
|
||||
|
||||
TEST_CASE("A printer bundle rejects bed assets whose extension the bed dialog does not allow", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", {}, {});
|
||||
nlohmann::json structure = bed_manifest("bad-ext");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.stl");
|
||||
set_printer_bed_asset(structure, "bed_custom_model", "bed/0/model.exe");
|
||||
const fs::path zip_file = bed.src / "bad-ext.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
|
||||
{"bed/0/texture.stl", kTextureBytes}, {"bed/0/model.exe", std::string("MZ")}});
|
||||
REQUIRE(import_bed(bed.bundle, zip_file, bed.data_root) != nullptr);
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, "model.exe"));
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.stl"));
|
||||
}
|
||||
|
||||
TEST_CASE("A manifest asset name that points at a preset does not drop that preset", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", {}, {});
|
||||
nlohmann::json structure = bed_manifest("name-clash");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "printer/BedPrinter.json");
|
||||
const fs::path zip_file = bed.src / "clash.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()}});
|
||||
REQUIRE(import_bed(bed.bundle, zip_file, bed.data_root) != nullptr);
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
|
||||
}
|
||||
|
||||
TEST_CASE("A declined printer bundle import leaves existing bed files alone", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const fs::path kept = bed.data_root / "user" / "default" / "_local" / "keep-bed" / "bed" / "texture.png";
|
||||
const std::string original("kept-texture");
|
||||
write_bytes(kept.string(), original);
|
||||
DynamicPrintConfig existing(bed.bundle.printers.default_preset().config);
|
||||
existing.option<ConfigOptionString>("printer_settings_id", true)->value = "BedPrinter";
|
||||
existing.option<ConfigOptionString>("inherits", true)->value.clear();
|
||||
bed.bundle.printers.load_preset((bed.src / "existing.json").string(), "_local/keep-bed/BedPrinter", std::move(existing),
|
||||
false);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", {}, {});
|
||||
nlohmann::json structure = bed_manifest("keep-bed");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
|
||||
set_printer_bed_asset(structure, "bed_custom_model", "bed/0/model.stl");
|
||||
const fs::path zip_file = bed.src / "keep.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
|
||||
{"bed/0/texture.png", kTextureBytes}, {"bed/0/model.stl", kModelBytes}});
|
||||
import_bed(bed.bundle, zip_file, bed.data_root, 0);
|
||||
// This import wrote nothing: the existing texture is unchanged and model.stl was not added.
|
||||
REQUIRE(fs::exists(kept));
|
||||
REQUIRE(read_file(kept) == original);
|
||||
CHECK_FALSE(fs::exists(kept.parent_path() / "model.stl"));
|
||||
CHECK_FALSE(fs::exists(kept.parent_path() / "0" / "model.stl"));
|
||||
}
|
||||
|
||||
TEST_CASE("A declined re-import does not replace bed bytes an earlier import wrote", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", {}, {});
|
||||
nlohmann::json structure = bed_manifest("reimport-no");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png");
|
||||
const std::string manifest = structure.dump();
|
||||
const std::string printer_bytes = read_file(printer_json);
|
||||
const fs::path first_zip = bed.src / "first.orca_printer";
|
||||
const fs::path second_zip = bed.src / "second.orca_printer";
|
||||
write_zip(first_zip, {{"printer/BedPrinter.json", printer_bytes}, {BUNDLE_STRUCTURE_JSON_NAME, manifest},
|
||||
{"bed/0/texture.png", kTextureBytes}});
|
||||
write_zip(second_zip, {{"printer/BedPrinter.json", printer_bytes}, {BUNDLE_STRUCTURE_JSON_NAME, manifest},
|
||||
{"bed/0/texture.png", kOtherBytes}});
|
||||
const Preset *saved = import_bed(bed.bundle, first_zip, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
const std::string installed = saved->config.opt_string("bed_custom_texture");
|
||||
REQUIRE(read_file(installed) == kTextureBytes);
|
||||
import_bed(bed.bundle, second_zip, bed.data_root, 0);
|
||||
REQUIRE(fs::exists(installed));
|
||||
REQUIRE(read_file(installed) == kTextureBytes);
|
||||
}
|
||||
|
||||
TEST_CASE("Export with bed assets disabled packs no bed files and writes no printer_asset", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "off.png").make_preferred().string();
|
||||
const std::string model = (bed.src / "off.stl").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
write_bytes(model, kModelBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", texture, model);
|
||||
const fs::path zip_file = bed.src / "off.orca_printer";
|
||||
const nlohmann::json structure = pack_bed_zip(zip_file, printer_json, bed.bundle.printers.default_preset().config,
|
||||
texture, model, "bed-off", false);
|
||||
REQUIRE_FALSE(structure.contains("printer_asset"));
|
||||
for (const std::string &name : zip_entry_names(zip_file))
|
||||
REQUIRE(name.compare(0, 4, "bed/") != 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Two printers in one bundle keep separate bed folders", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const std::string tex_a = (bed.src / "a.png").make_preferred().string();
|
||||
const std::string mod_a = (bed.src / "a.stl").make_preferred().string();
|
||||
const std::string tex_b = (bed.src / "b.png").make_preferred().string();
|
||||
const std::string mod_b = (bed.src / "b.stl").make_preferred().string();
|
||||
write_bytes(tex_a, kTextureBytes);
|
||||
write_bytes(mod_a, kModelBytes);
|
||||
write_bytes(tex_b, kOtherBytes);
|
||||
write_bytes(mod_b, std::string("stl\x00other", 9));
|
||||
const fs::path json_a = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "PrinterA.json",
|
||||
"PrinterA", tex_a, mod_a);
|
||||
const fs::path json_b = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "PrinterB.json",
|
||||
"PrinterB", tex_b, mod_b);
|
||||
nlohmann::json structure = nlohmann::json::object();
|
||||
structure["id"] = "two-printers";
|
||||
structure["bundle_type"] = "printer config bundle";
|
||||
structure["printer_config"] = nlohmann::json::array({"printer/PrinterA.json", "printer/PrinterB.json"});
|
||||
DynamicPrintConfig config_a(bed.bundle.printers.default_preset().config);
|
||||
config_a.option<ConfigOptionString>("bed_custom_texture", true)->value = tex_a;
|
||||
config_a.option<ConfigOptionString>("bed_custom_model", true)->value = mod_a;
|
||||
DynamicPrintConfig config_b(bed.bundle.printers.default_preset().config);
|
||||
config_b.option<ConfigOptionString>("bed_custom_texture", true)->value = tex_b;
|
||||
config_b.option<ConfigOptionString>("bed_custom_model", true)->value = mod_b;
|
||||
const fs::path zip_file = bed.src / "two.orca_printer";
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
const std::string bytes_a = read_file(json_a);
|
||||
const std::string bytes_b = read_file(json_b);
|
||||
REQUIRE(open_zip_writer(&zip, zip_file.string()));
|
||||
REQUIRE(mz_zip_writer_add_mem(&zip, "printer/PrinterA.json", bytes_a.data(), bytes_a.size(), MZ_DEFAULT_COMPRESSION));
|
||||
REQUIRE(mz_zip_writer_add_mem(&zip, "printer/PrinterB.json", bytes_b.data(), bytes_b.size(), MZ_DEFAULT_COMPRESSION));
|
||||
REQUIRE(append_printer_bed_assets(zip, config_a, structure, "printer/PrinterA.json", 0));
|
||||
REQUIRE(append_printer_bed_assets(zip, config_b, structure, "printer/PrinterB.json", 1));
|
||||
const std::string manifest = structure.dump();
|
||||
REQUIRE(mz_zip_writer_add_mem(&zip, BUNDLE_STRUCTURE_JSON_NAME, manifest.data(), manifest.size(),
|
||||
MZ_DEFAULT_COMPRESSION));
|
||||
REQUIRE(mz_zip_writer_finalize_archive(&zip));
|
||||
REQUIRE(close_zip_writer(&zip));
|
||||
REQUIRE(structure["printer_asset"]["printer/PrinterA.json"]["bed_custom_texture"] == "bed/0/texture.png");
|
||||
REQUIRE(structure["printer_asset"]["printer/PrinterB.json"]["bed_custom_texture"] == "bed/1/texture.png");
|
||||
ScopedDataDir scoped(bed.data_root);
|
||||
AppConfig app_config;
|
||||
std::vector<std::string> files{zip_file.string()};
|
||||
bed.bundle.import_presets(files, [](std::string const &) { return 1; },
|
||||
ForwardCompatibilitySubstitutionRule::Disable, app_config);
|
||||
const Preset *saved_a = nullptr;
|
||||
const Preset *saved_b = nullptr;
|
||||
for (const Preset &preset : bed.bundle.printers.get_presets()) {
|
||||
if (preset.file.empty())
|
||||
continue;
|
||||
if (get_preset_bare_name(preset.name) == "PrinterA")
|
||||
saved_a = &preset;
|
||||
if (get_preset_bare_name(preset.name) == "PrinterB")
|
||||
saved_b = &preset;
|
||||
}
|
||||
REQUIRE(saved_a != nullptr);
|
||||
REQUIRE(saved_b != nullptr);
|
||||
const std::string got_tex_a = saved_a->config.opt_string("bed_custom_texture");
|
||||
const std::string got_tex_b = saved_b->config.opt_string("bed_custom_texture");
|
||||
REQUIRE(fs::path(got_tex_a).parent_path().filename() == "0");
|
||||
REQUIRE(fs::path(got_tex_b).parent_path().filename() == "1");
|
||||
REQUIRE(got_tex_a != got_tex_b);
|
||||
REQUIRE(read_file(got_tex_a) == kTextureBytes);
|
||||
REQUIRE(read_file(got_tex_b) == kOtherBytes);
|
||||
REQUIRE(read_file(saved_a->config.opt_string("bed_custom_model")) == kModelBytes);
|
||||
REQUIRE(read_file(saved_b->config.opt_string("bed_custom_model")) == std::string("stl\x00other", 9));
|
||||
}
|
||||
|
||||
TEST_CASE("A printer_asset entry keyed to a printer not in the zip is ignored", "[Preset][Bundle]")
|
||||
{
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "kept.png").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", texture, {});
|
||||
nlohmann::json structure = bed_manifest("ghost-key");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/texture.png", "printer/Ghost.json");
|
||||
const fs::path zip_file = bed.src / "ghost.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
|
||||
{"bed/0/texture.png", kOtherBytes}});
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, "texture.png"));
|
||||
}
|
||||
|
||||
TEST_CASE("Keyed printer_asset rejects malformed bed paths", "[Preset][Bundle]")
|
||||
{
|
||||
const char *bad_names[] = {"bed/texture.png", "bed/x/texture.png", "bed/0/../texture.png", "bed/0/model.png"};
|
||||
const std::string marker = "PWNED-BED-ASSET-MARKER";
|
||||
for (const char *name : bad_names) {
|
||||
DYNAMIC_SECTION("rejects " << name) {
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "kept.png").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config,
|
||||
bed.src / "BedPrinter.json", "BedPrinter", texture, {});
|
||||
nlohmann::json structure = bed_manifest("bad-keyed");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", name);
|
||||
std::vector<std::pair<std::string, std::string>> entries = {
|
||||
{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
|
||||
{name, marker}};
|
||||
const fs::path zip_file = bed.src / "bad-keyed.orca_printer";
|
||||
write_zip(zip_file, entries);
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
|
||||
if (fs::exists(bed.data_root))
|
||||
for (fs::recursive_directory_iterator it(bed.data_root), end; it != end; ++it)
|
||||
if (fs::is_regular_file(*it))
|
||||
CHECK(read_file(*it).find(marker) == std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("field name does not match file kind") {
|
||||
BedRoots bed;
|
||||
const std::string texture = (bed.src / "kept.png").make_preferred().string();
|
||||
write_bytes(texture, kTextureBytes);
|
||||
const fs::path printer_json = write_printer_json(bed.bundle.printers.default_preset().config, bed.src / "BedPrinter.json",
|
||||
"BedPrinter", texture, {});
|
||||
nlohmann::json structure = bed_manifest("kind-mismatch");
|
||||
set_printer_bed_asset(structure, "bed_custom_texture", "bed/0/model.stl");
|
||||
const fs::path zip_file = bed.src / "kind.orca_printer";
|
||||
write_zip(zip_file, {{"printer/BedPrinter.json", read_file(printer_json)}, {BUNDLE_STRUCTURE_JSON_NAME, structure.dump()},
|
||||
{"bed/0/model.stl", kModelBytes}});
|
||||
const Preset *saved = import_bed(bed.bundle, zip_file, bed.data_root);
|
||||
REQUIRE(saved != nullptr);
|
||||
REQUIRE(saved->config.opt_string("bed_custom_texture") == texture);
|
||||
CHECK_FALSE(any_filename_contains(bed.data_root, "model.stl"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A saved printer preset reloads the tool layout it was saved with", "[Preset][Bundle][IMEX]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
|
||||
Reference in New Issue
Block a user