Merge branch 'main' into weilun/speed_dial

This commit is contained in:
Lam Wei Lun
2026-09-10 16:39:47 +08:00
75 changed files with 13507 additions and 1154 deletions
+12 -1
View File
@@ -42,6 +42,9 @@ namespace Slic3r {
static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest";
static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile";
constexpr const char* CONFIG_ORCA_UPDATER_URL = "orca_updater_url";
static const std::string MODELS_STR = "models";
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
@@ -635,6 +638,11 @@ void AppConfig::set_defaults()
set_bool("use_printer_agents", false);
}
if (get("enable_ota").empty())
{
set_bool("enable_ota", false);
}
// Remove legacy window positions/sizes
erase("app", "main_frame_maximized");
erase("app", "main_frame_pos");
@@ -1815,7 +1823,10 @@ std::string AppConfig::version_check_url() const
std::string AppConfig::profile_update_url() const
{
return PROFILE_UPDATE_URL;
std::string orca_updater_url = get(CONFIG_ORCA_UPDATER_URL);
if (orca_updater_url.empty())
return PROFILE_UPDATE_URL;
return orca_updater_url;
}
bool AppConfig::exists()
+2
View File
@@ -372,6 +372,8 @@ set(lisbslic3r_sources
Preset.hpp
PrincipalComponents2D.cpp
PrincipalComponents2D.hpp
PublishSettings.cpp
PublishSettings.hpp
PrintApply.cpp
PrintBase.cpp
PrintBase.hpp
+179 -10
View File
@@ -642,6 +642,11 @@ bool bbs_is_valid_object_type(const std::string& type)
namespace Slic3r {
bool is_published_3mf_flag(const std::string &value)
{
return value == "1";
}
void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
if (!result) return;
@@ -1178,6 +1183,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// add backup & restore logic
bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector<Preset*>& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr,
BBLProject* project = nullptr, int plate_id = 0);
// A minimal published 3MF carries no slicer tags (any tag would make old receivers show
// a baked-in, wrong "old version" popup on their geometry-only fallback), so it
// classifies as From_Other. It is still a fully structured OrcaSlicer file though:
// identified by its own metadata, it keeps BBS-grade geometry handling (no instance
// splitting, no transform baking, no renaming) in this build. Old receivers without the
// publish feature don't know the metadata and take their third-party geometry path.
// Reads the parse-time metadata: the model XML carries it before its resources, while
// m_model->model_info is only filled in after the whole XML has been parsed.
bool _is_published_3mf() const {
const auto it = this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG);
return it != this->model_info.metadata_items.end() && is_published_3mf_flag(it->second);
}
bool _is_svg_shape_file(const std::string &filename) const;
bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function<bool (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)>, bool restore = false);
bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler);
@@ -2002,7 +2021,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
lock.close();
if (!m_is_bbl_3mf) {
if (!m_is_bbl_3mf && !_is_published_3mf()) {
// if the 3mf was not produced by OrcaSlicer and there is more than one instance,
// split the object in as many objects as instances
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance");
@@ -3566,7 +3585,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_index_paths.insert({ object.first.second, object.first.first});
}
if (!m_is_bbl_3mf) {
if (!m_is_bbl_3mf && !_is_published_3mf()) {
// if the 3mf was not produced by OrcaSlicer and there is only one object,
// set the object name to match the filename
if (m_model->objects.size() == 1)
@@ -5289,7 +5308,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats);
if (!m_is_bbl_3mf) {
if (!m_is_bbl_3mf && !_is_published_3mf()) {
// if the 3mf was not produced by OrcaSlicer and there is only one instance,
// bake the transformation into the geometry to allow the reload from disk command
// to work properly
@@ -5935,6 +5954,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool m_save_gcode { false }; // whether to save gcode for normal save
bool m_skip_model { false }; // skip model when exporting .gcode.3mf
bool m_skip_auxiliary { false }; // skip normal axuiliary files
bool m_minimal_published { false }; // published 3MF: omit the project config, the embedded preset files and the slicer tags
bool m_use_loaded_id { false }; // whether to use loaded id for identify_id
bool m_share_mesh { false }; // whether to share mesh between objects
std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE;
@@ -6034,6 +6054,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_skip_auxiliary = store_params.strategy & SaveStrategy::SkipAuxiliary;
m_share_mesh = store_params.strategy & SaveStrategy::ShareMesh;
m_from_backup_save = store_params.strategy & SaveStrategy::Backup;
m_minimal_published = store_params.strategy & SaveStrategy::MinimalPublished;
m_use_loaded_id = store_params.strategy & SaveStrategy::UseLoadedId;
@@ -6443,7 +6464,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// Adds slic3r print config file ("Metadata/Slic3r_PE.config").
// This file contains the content of FullPrintConfig / SLAFullPrintConfig.
if (config != nullptr) {
// Omitted for minimal published 3MF: OrcaSlicer versions without the publish feature
// then fall back to importing the geometry only, and new versions read the published
// payload from the model metadata instead.
if (config != nullptr && !m_minimal_published) {
// BBS: change to json format
// if (!_add_print_config_file_to_archive(archive, *config)) {
if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; }
@@ -6456,8 +6480,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (cb_cancel) return false;
}
// BBS: add project config
if (project_presets.size() > 0) {
// BBS: add project config (omitted for minimal published 3MF)
if (!m_minimal_published && project_presets.size() > 0) {
// BBS: add project embedded preset files
_add_project_embedded_presets_to_archive(archive, model, project_presets);
@@ -6929,10 +6953,31 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// Orca: PRIVACY: do not store creation & modification date in 3mf
metadata_item_map[BBL_CREATION_DATE_TAG] = "";
metadata_item_map[BBL_MODIFICATION_TAG] = "";
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION.
// A minimal published 3MF writes no slicer tags at all: any tag would route old
// receivers onto a geometry-only fallback whose baked-in popup misreports the
// file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify
// as From_Other and import the geometry silently.
if (m_minimal_published) {
// metadata_item_map is seeded from the input file's metadata_items above, so a
// project opened from a regular Orca/BBS 3MF still carries the slicer-identifying
// tags it came with. Erase every one of them - not just the two most common -
// so a published 3MF is fully tag-less: old receivers classify it as From_Other
// and import the geometry silently instead of showing a baked-in "old version"
// popup, and no version marker survives to seed a later re-save.
metadata_item_map.erase(BBL_APPLICATION_TAG);
metadata_item_map.erase(ORCASLICER_TAG);
metadata_item_map.erase(BBS_3MF_VERSION);
metadata_item_map.erase(BBS_3MF_VERSION1);
} else {
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
}
}
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
// The Bambu 3MF version marker is part of the slicer identity: omit it for a minimal
// published file along with the tags erased above (skipping the overwrite alone would
// leave the value the source file seeded into metadata_item_map).
if (!m_minimal_published)
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
if (!model.mk_name.empty()) {
metadata_item_map[BBL_MAKERLAB_TAG] = xml_escape(model.mk_name);
@@ -6955,7 +7000,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
BOOST_LOG_TRIVIAL(info) << "bbs_3mf: save key= " << item.first << ", value = " << item.second;
stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">"
<< xml_escape(item.second) << "</" << METADATA_TAG << ">\n";
if (item.first == BBL_APPLICATION_TAG) {
if (item.first == BBL_APPLICATION_TAG && !m_minimal_published) {
// The OrcaSlicer tag is only written for files that carry the Application
// tag, which a minimal published 3MF erases (see the map assignment above):
// the explicit !m_minimal_published guard keeps the tag-less guarantee from
// depending on that erase happening to run first.
stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">"
<< xml_escape(SoftFever_VERSION) << "</" << METADATA_TAG << ">\n";
}
@@ -9134,6 +9183,126 @@ std::string bbs_3mf_get_thumbnail(const char *path)
return data;
}
namespace {
// Parses just the model-file <metadata> elements, mirroring the importer's
// _handle_start_metadata/_handle_end_metadata (attribute-order independent, entity-unescaped,
// whitespace tolerant). Stops the parser as soon as the published flag node is read so the
// geometry/resources that follow are skipped, which keeps the per-file cost small.
struct PublishedXmlProbe
{
XML_Parser parser{nullptr};
bool in_metadata{false};
bool found{false};
bool published{false};
std::string curr_name;
std::string curr_value;
static std::string attribute(const char** attrs, const char* key)
{
if (attrs == nullptr)
return std::string();
// expat hands the attrs as a NULL-terminated {name, value, ...} array.
for (unsigned int a = 0; attrs[a] != nullptr; a += 2)
if (::strcmp(attrs[a], key) == 0 && attrs[a + 1] != nullptr)
return attrs[a + 1];
return std::string();
}
static void XMLCALL start(void* user_data, const char* name, const char** attrs)
{
auto* self = static_cast<PublishedXmlProbe*>(user_data);
if (::strcmp(name, METADATA_TAG) == 0) {
self->in_metadata = true;
self->curr_name = attribute(attrs, NAME_ATTR);
self->curr_value.clear();
} else {
self->in_metadata = false;
}
}
static void XMLCALL characters(void* user_data, const XML_Char* s, int len)
{
auto* self = static_cast<PublishedXmlProbe*>(user_data);
if (self->in_metadata)
self->curr_value.append(s, len);
}
static void XMLCALL end(void* user_data, const char* name)
{
auto* self = static_cast<PublishedXmlProbe*>(user_data);
if (!self->in_metadata || ::strcmp(name, METADATA_TAG) != 0)
return;
self->in_metadata = false;
if (self->curr_name == ORCA_PUBLISHED_TAG) {
self->published = is_published_3mf_flag(xml_unescape(self->curr_value));
self->found = true;
if (self->parser != nullptr)
XML_StopParser(self->parser, false);
}
}
};
} // namespace
bool bbs_3mf_is_published(const std::string &path)
{
mz_zip_archive archive;
mz_zip_zero_struct(&archive);
struct close_lock
{
mz_zip_archive *archive;
void close()
{
if (archive) {
close_zip_reader(archive);
archive = nullptr;
}
}
~close_lock() { close(); }
} lock{&archive};
if (!open_zip_reader(&archive, path))
return false;
// Read just the model XML (the metadata node sits before the resources, so the probe below
// stops early) rather than by a raw substring match; no geometry parsing.
int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0);
if (index < 0)
return false;
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&archive, index, &stat))
return false;
std::string xml(stat.m_uncomp_size, '\0');
if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0))
return false;
XML_Parser parser = XML_ParserCreate(nullptr);
if (parser == nullptr)
return false;
PublishedXmlProbe probe;
probe.parser = parser;
XML_SetUserData(parser, &probe);
XML_SetElementHandler(parser, PublishedXmlProbe::start, PublishedXmlProbe::end);
XML_SetCharacterDataHandler(parser, PublishedXmlProbe::characters);
// Never resolve external entities from a file we are only probing.
XML_SetExternalEntityRefHandler(parser, nullptr);
XML_SetEntityDeclHandler(parser, nullptr);
const XML_Status status = XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), 1);
// XML_StopParser(parser, false) from the end handler makes XML_Parse return
// XML_STATUS_ERROR with XML_ERROR_ABORTED - treat that as success (we stopped on the flag).
const bool parse_ok = (status == XML_STATUS_OK) ||
(XML_GetErrorCode(parser) == XML_ERROR_ABORTED && probe.found);
XML_ParserFree(parser);
if (!parse_ok)
return false;
return probe.published;
}
bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version)
{
CNumericLocalesSetter locales_setter;
+19
View File
@@ -159,12 +159,28 @@ enum class SaveStrategy
SkipAuxiliary = 1 << 9,
UseLoadedId = 1 << 10,
ShareMesh = 1 << 11,
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
// production-extension value.
MinimalPublished = 1 << 13,
SplitModel = 0x1000 | ProductionExt,
Encrypted = SecureContentExt | SplitModel,
Backup = 0x10000 | WithGcode | Silence | SkipStatic | SplitModel,
};
// Model metadata keys of a "published" 3MF (see MinimalPublished): the flag marks a minimal,
// tag-less publish export, the others carry the author-selected settings payload. Namespaced
// with the "orca_published" prefix because metadata_items round-trips verbatim through other
// slicers, where a bare "published" key could collide.
inline constexpr const char *ORCA_PUBLISHED_TAG = "orca_published";
inline constexpr const char *ORCA_PUBLISHED_KEYS_TAG = "orca_published_keys";
inline constexpr const char *ORCA_PUBLISHED_MATERIAL_TAG = "orca_published_material_keys";
inline constexpr const char *ORCA_PUBLISHED_CONFIG_TAG = "orca_published_config";
// Published files are produced with "1". The importer and the GUI loader both gate on this
// exact value, so a "0"/"false"/unknown value is rejected consistently.
bool is_published_3mf_flag(const std::string &value);
inline SaveStrategy operator | (SaveStrategy lhs, SaveStrategy rhs)
{
using T = std::underlying_type_t <SaveStrategy>;
@@ -277,6 +293,9 @@ extern bool load_bbs_3mf(const char* path, DynamicPrintConfig* config, ConfigSub
extern std::string bbs_3mf_get_thumbnail(const char * path);
// Lightweight check: does this 3mf carry the "published" (orca_published == "1") marker? Only reads the 3D/3dmodel.model metadata node
extern bool bbs_3mf_is_published(const std::string &path);
extern bool load_gcode_3mf_from_stream(std::istream & data, DynamicPrintConfig* config, Model* model, PlateDataPtrs* plate_data_list,
Semver* file_version);
+46
View File
@@ -3598,6 +3598,15 @@ void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlocker
this->set(selector);
}
void FacetsAnnotation::remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map)
{
if (empty()) return;
TriangleSelector selector(mv.mesh());
selector.deserialize(m_data, false);
selector.remap_triangle_state(state_map);
this->set(selector);
}
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
EnforcerBlockerType max_type,
EnforcerBlockerType to_delete_filament,
@@ -3862,6 +3871,43 @@ bool model_has_advanced_features(const Model &model)
return false;
}
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations)
{
if (slot_relocations.empty())
return;
// Paint states and the object/volume "extruder" configs store one-based slot numbers
// (see Sidebar::on_action_add_filament's insertion remap for the same encoding).
std::map<int, int> one_based_slots;
for (const auto &[from, to] : slot_relocations)
one_based_slots.emplace(from + 1, to + 1);
EnforcerBlockerStateMap paint_state_map;
for (size_t state = 0; state < paint_state_map.size(); ++state)
paint_state_map[state] = EnforcerBlockerType(state);
for (const auto &[one_based_from, one_based_to] : one_based_slots) {
assert(one_based_from >= 0 && size_t(one_based_from) < paint_state_map.size());
assert(one_based_to > 0 && size_t(one_based_to) < paint_state_map.size());
paint_state_map[size_t(one_based_from)] = EnforcerBlockerType(one_based_to);
}
auto remap_extruder_config = [&one_based_slots](ModelConfig &config) -> bool {
const auto it = config.has("extruder") ? one_based_slots.find(config.extruder()) : one_based_slots.end();
if (it == one_based_slots.end())
return false;
config.set("extruder", it->second);
return true;
};
for (ModelObject *object : model.objects) {
remap_extruder_config(object->config);
for (ModelVolume *volume : object->volumes) {
remap_extruder_config(volume->config);
volume->mmu_segmentation_facets.remap_states(*volume, paint_state_map);
}
}
}
#ifndef NDEBUG
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
void check_model_ids_validity(const Model &model)
+11
View File
@@ -745,6 +745,10 @@ public:
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
// Relabel painted filament indices according to state_map (old state value -> new state
// value; untouched states keep their identity). Used when published-3MF import relocates
// mixed-filament definitions onto new slot numbers.
void remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map);
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
bool empty() const { return m_data.triangles_to_split.empty(); }
@@ -1790,6 +1794,13 @@ bool model_has_multi_part_objects(const Model &model);
// If the model has advanced features, then it cannot be processed in simple mode.
bool model_has_advanced_features(const Model &model);
// Remap the model's filament-slot references after a published-3MF import relocated
// mixed-filament definitions onto new slot numbers: object/volume "extruder" configs and
// multi-material color-painting states (paint state stores the one-based slot number).
// slot_relocations maps the author's zero-based slot number to its final zero-based slot;
// entries are applied simultaneously (no chained lookups), untouched slots keep everything.
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations);
#ifndef NDEBUG
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
void check_model_ids_validity(const Model &model);
+69
View File
@@ -3062,6 +3062,75 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
this->get_selected_preset().save(nullptr);
}
// A detached standalone preset for the Full Publish receiver: create a user preset holding
// the full resolved filament config (no inheritance, no vendor/alias links), parentless.
// Note: universal printer compatibility is not enforced here - callers apply
// make_publish_universal() to the config before handing it over when they need it.
// Mirrors save_current_preset(detach=true)'s creation branch but does not force-select or
// diff against a parent; the caller decides whether to select it.
// The published entry's filament_id is forwarded so user bases keep their stable
// material grouping (get_filament_presets() groups user bases by filament_id).
// The copy is a project-embedded preset: it lives inside the loaded project only
// (serialized into the saved .3mf, restored by load_project_embedded_presets) and
// never touches the user's library directory; Preset::save() early-returns for
// embedded presets, so persistence is skipped here too.
// Returns the final (uniquified) name; on collision "<base>" -> "<base> (Published)" ->
// "<base> (Published 2)" ...
std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
const std::string &filament_id)
{
if (name_base.empty())
return std::string();
Preset stored(m_type, name_base);
stored.config = std::move(config);
stored.filament_id = filament_id;
// Uniquify verbatim; only on collision append " (Published)" then " (Published 2)".
const std::string base_name = name_base;
std::string final_name = base_name;
auto exists = [this](const std::string &candidate) -> bool {
const auto it = this->find_preset_internal(candidate);
return it != m_presets.end() && it->name == candidate;
};
if (exists(final_name)) {
final_name = base_name + " (Published)";
for (int i = 2; exists(final_name); ++i)
final_name = base_name + " (Published " + std::to_string(i) + ")";
}
// Creation branch of save_current_preset(detach=true), without its selection side
// effects or project-embedded path.
lock();
const auto it = this->find_preset_internal(final_name);
if (m_presets.begin() + m_idx_selected >= it)
++m_idx_selected;
Preset &preset = *m_presets.insert(it, stored);
preset.name = final_name;
preset.vendor = nullptr;
preset.alias.clear();
preset.renamed_from.clear();
preset.m_excluded_from.clear();
preset.setting_id.clear();
preset.inherits().clear();
preset.version = Semver::parse(SoftFever_VERSION).value_or(Semver());
preset.is_default = false;
preset.is_system = false;
preset.is_external = false;
preset.bundle_id.clear();
preset.file = this->path_for_preset(preset);
preset.is_visible = true;
preset.is_project_embedded = true;
if (m_type == Preset::TYPE_PRINT)
preset.config.option<ConfigOptionString>("print_settings_id", true)->value = final_name;
else if (m_type == Preset::TYPE_FILAMENT)
preset.config.option<ConfigOptionStrings>("filament_settings_id", true)->values[0] = final_name;
else if (m_type == Preset::TYPE_PRINTER)
preset.config.option<ConfigOptionString>("printer_settings_id", true)->value = final_name;
unlock();
return final_name;
}
bool PresetCollection::delete_current_preset()
{
Preset &selected = this->get_selected_preset();
+16
View File
@@ -631,6 +631,22 @@ public:
// All presets are marked as not modified and the new preset is activated.
//BBS: add project embedded preset logic
void save_current_preset(const std::string &new_name, bool detach = false, bool save_to_project = false, Preset* _curr_preset = nullptr);
// Insert a standalone user preset holding the full resolved config (no inheritance,
// no vendor links): the libslic3r equivalent of "Detach from parent". Takes a
// resolved config, clears parent/vendor/alias metadata, stamps filament_settings_id.
// Unlike save_current_preset it does not force-select or diff against a parent.
// Used by the published-3MF Full Publish path. The optional filament_id seeds the
// preset's stable material grouping (get_filament_presets groups user bases by
// filament_id); the published entry's filament_id is forwarded so the copy keeps
// the author's grouping.
// The copy is a project-embedded preset ("Preset Inside Project"): it lives inside
// the loaded project only, is serialized into the saved .3mf via
// get_current_project_embedded_presets(), and is never written to the user's
// library directory.
// Returns the final (uniquified) name; on collision the suffix rule is:
// "<base>" -> "<base> (Published)" -> "<base> (Published 2)" ...
std::string add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
const std::string &filament_id = std::string());
// Delete the current preset, activate the first visible preset.
// returns true if the preset was deleted successfully.
File diff suppressed because it is too large Load Diff
+29 -3
View File
@@ -4,9 +4,11 @@
#include "Preset.hpp"
#include "PresetCacheFormat.hpp"
#include "AppConfig.hpp"
#include "PublishSettings.hpp"
#include "enum_bitmask.hpp"
#include <memory>
#include <map>
#include <set>
#include <shared_mutex>
#include <unordered_map>
@@ -168,6 +170,30 @@ struct PresetBundleMetadata
}
};
// A "published" 3MF project: keeps the user's currently-selected presets and overlays only the
// author-selected published keys onto the edited presets.
struct PublishedConfig
{
bool published = false;
std::vector<std::string> published_keys;
// Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
// Partial entries are gated by the author's optional type requirement and written onto the
// slot's stored preset in place; full entries instead detach (see PublishedMaterialEntry in
// PublishSettings.hpp).
std::vector<PublishedMaterialEntry> material_keys;
// Keys that could not be applied (missing on the user's machine or vector size mismatch),
// filled in by load_config_file_config for notification purposes.
std::vector<std::string> skipped_keys;
// Human-readable notices of the slot material replacements performed while loading a
// published project, for the load notification.
std::vector<std::string> material_replacements;
// Mixed-filament entries that had to be moved off their authored slot on load (a real,
// physical filament occupied it): maps the author's zero-based slot number to its final
// zero-based slot. Consumers (e.g. model extruder/color-painting remapping) use this to
// keep geometry references pointing at the relocated definitions.
std::map<int, int> mixed_slot_relocations;
};
// Bundle of Print + Filament + Printer presets.
class PresetBundle
{
@@ -464,8 +490,8 @@ public:
// Load configuration that comes from a model file containing configuration, such as 3MF et al.
// This method is called by the Plater.
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver())
{ this->load_config_file_config(name, true, std::move(config), file_version); }
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver(), PublishedConfig *published_config = nullptr)
{ this->load_config_file_config(name, true, std::move(config), file_version, false, published_config); }
// Load an external config file containing the print, filament and printer presets.
// Instead of a config file, a G-code may be loaded containing the full set of parameters.
@@ -646,7 +672,7 @@ private:
// Load print, filament & printer presets from a config. If it is an external config, then the name is extracted from the external path.
// and the external config is just referenced, not stored into user profile directory.
// If it is not an external config, then the config will be stored into the user profile directory.
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false);
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false, PublishedConfig *published_config = nullptr);
/*ConfigSubstitutions load_config_file_config_bundle(
const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
+315
View File
@@ -0,0 +1,315 @@
#include "PublishSettings.hpp"
#include "PresetBundle.hpp"
#include "Preset.hpp"
#include "PrintConfig.hpp"
#include "MaterialType.hpp"
#include <boost/log/trivial.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <map>
#include <set>
namespace Slic3r {
std::string publish_base_key(const std::string &key)
{
const size_t pos = key.find('#');
return pos == std::string::npos ? key : key.substr(0, pos);
}
// Parse the trailing "#N" variant index ("retraction_length#2" -> 2). Returns -1 when the key
// carries no '#' separator or its suffix is malformed; mirrors the importer's strict parse
// (PresetBundle.cpp) so the export side rejects the same variants the receiver would skip.
static int publish_variant_index(const std::string &key, const std::string &base_key)
{
if (key.size() <= base_key.size() || key.compare(0, base_key.size(), base_key) != 0 || key[base_key.size()] != '#')
return -1;
const std::string suffix = key.substr(base_key.size() + 1);
if (suffix.empty())
return -1;
int idx = 0;
for (const char c : suffix) {
if (c < '0' || c > '9')
return -1;
idx = idx * 10 + (c - '0');
if (idx > 1000000) // overflow guard; real vector sizes are tiny
return -1;
}
return idx;
}
std::string normalize_filament_type(const std::string& type)
{
if (type.empty())
return type;
if (MaterialType::find(type) != nullptr)
return type;
// "PLA High Speed" -> "PLA": strip a space-separated modifier, but keep dash-separated
// types like "PA-CF" / "PETG-CF" intact (they are distinct materials, not modifiers).
const size_t sep = type.find(' ');
if (sep != std::string::npos) {
const std::string base = type.substr(0, sep);
if (MaterialType::find(base) != nullptr)
return base;
}
return type;
}
void make_publish_universal(DynamicPrintConfig &config)
{
// Lists: empty => compatible with every printer / every print preset. Conditions:
// empty so a leftover expression left behind by the baseline clone can never
// re-narrow the match (see is_compatible_with_printer, Preset.cpp:840). All four
// keys exist on filament presets; nil-guard for hand-crafted future schemas.
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_printers", false))
opt->values.clear();
if (auto *opt = config.opt<ConfigOptionStrings>("compatible_prints", false))
opt->values.clear();
if (auto *opt = config.opt<ConfigOptionString>("compatible_printers_condition", false))
opt->value.clear();
if (auto *opt = config.opt<ConfigOptionString>("compatible_prints_condition", false))
opt->value.clear();
}
std::string publish_material_base_name(const std::string &preset_name)
{
if (preset_name.empty())
return preset_name;
const size_t at = preset_name.find('@');
std::string base = (at == std::string::npos) ? preset_name : preset_name.substr(0, at);
boost::trim_right(base);
return base;
}
const std::set<std::string>& publish_structural_keys()
{
// Non-publishable keys: the *_settings_id keys are also in PresetCollection::skipped_in_dirty
// (Preset.cpp) / stripped from configs (profile_print_params_same); publishing them would
// rewrite the user's preset inheritance/structure.
static const std::set<std::string> structural_keys = {
"printer_settings_id", "filament_settings_id", "print_settings_id",
"sla_print_settings_id", "sla_material_settings_id",
"compatible_printers", "compatible_prints",
"compatible_printers_condition", "compatible_prints_condition",
"default_filament_profile", "default_print_profile",
"default_sla_print_profile", "default_sla_material_profile",
"extruder_count", "bed_shape",
"inherits", "inherits_group",
"printer_technology", "printer_model", "printer_variant",
"physical_printer_settings_id", "filament_ids",
"different_settings_to_system"
};
return structural_keys;
}
const std::set<std::string>& publish_mixed_keys()
{
// Must match PresetBundle's s_project_options mixed-color group (PresetBundle.cpp): these
// are project-level parallel per-slot arrays, not filament-preset options, so the import
// material pass applies them into project_config instead of a filament preset config.
static const std::set<std::string> mixed_keys = {
"filament_is_mixed",
"filament_mixed_components",
"filament_mixed_sublayer_ratios",
"filament_mixed_gradient",
"filament_mixed_gradient_range",
"filament_mixed_gradient_curve",
"filament_mixed_gradient_per_part"
};
return mixed_keys;
}
// The printer tab's "Retraction" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order.
// KEEP IN SYNC with that optgroup: the published-3MF printer allowlist is built from these
// lists, so any key shown there must be publishable here (and vice versa).
const std::vector<PublishablePrinterOption>& publishable_printer_retraction_options()
{
static const std::vector<PublishablePrinterOption> options = {
{ "retraction_length", "printer_extruder_retraction#length" },
{ "retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart" },
{ "retraction_speed", "printer_extruder_retraction#retraction-speed" },
{ "deretraction_speed", "printer_extruder_retraction#deretraction-speed" },
{ "retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold" },
{ "retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change" },
{ "wipe", "printer_extruder_retraction#wipe-while-retracting" },
{ "wipe_distance", "printer_extruder_retraction#wipe-distance" },
{ "retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe" },
{ "retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe" },
};
return options;
}
// The printer tab's "Z-Hop" optgroup (TabPrinter::build_fff, Tab.cpp), in tab order. KEEP IN
// SYNC with that optgroup, same as publishable_printer_retraction_options().
const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options()
{
static const std::vector<PublishablePrinterOption> options = {
{ "retract_lift_enforce", "printer_extruder_z_hop#on-surfaces" },
{ "z_hop_types", "printer_extruder_z_hop#z-hop-type" },
{ "z_hop", "printer_extruder_z_hop#z-hop-height" },
{ "travel_slope", "printer_extruder_z_hop#traveling-angle" },
{ "retract_lift_above", "printer_extruder_z_hop#only-lift-z-above" },
{ "retract_lift_below", "printer_extruder_z_hop#only-lift-z-below" },
};
return options;
}
const std::set<std::string>& publishable_printer_keys()
{
// Union of the two optgroups; "Retraction when switching material" keys are excluded
// (toolchange retraction is device/profile territory, not a publishable behavior tweak).
static const std::set<std::string> printer_keys = [] {
std::set<std::string> keys;
for (const PublishablePrinterOption &opt : publishable_printer_retraction_options())
keys.insert(opt.key);
for (const PublishablePrinterOption &opt : publishable_printer_z_hop_options())
keys.insert(opt.key);
return keys;
}();
return printer_keys;
}
std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle)
{
std::set<std::string> keys;
// Union the dirty keys of each collection's edited preset (filaments may span multiple
// slots); feeds only the Publish dialog's pre-check.
for (const std::string& key : bundle.prints.current_dirty_options(true))
keys.insert(key);
for (const std::string& key : bundle.printers.current_dirty_options(true))
keys.insert(key);
for (const std::string& key : bundle.filaments.current_dirty_options(true))
keys.insert(key);
return std::vector<std::string>(keys.begin(), keys.end());
}
DynamicPrintConfig filter_published_config(
const DynamicPrintConfig &full_config,
const std::vector<std::string> &published_keys,
const std::vector<PublishedMaterialEntry> &material_keys)
{
DynamicPrintConfig filtered;
std::set<std::string> base_keys_to_include;
// Never masked (whole-vector serialization): identity, plate geometry, process keys and
// printer keys without a "#N" variant.
std::set<std::string> mask_exempt_keys;
// Material entries: base key -> author slots whose values must survive; other slots are
// masked to their defaults so a publish (partial or full) does not leak unrelated slot
// data.
std::map<std::string, std::set<int>> slot_mask_map;
// 1. Mandatory material identity & slot count keys for 3MF validation/normalization
// (filament_ids: exported for validation, denylisted on apply - see publish_structural_keys).
static const std::vector<std::string> s_material_identity_keys = {
"filament_colour",
"filament_type",
"filament_vendor",
"filament_ids",
"filament_diameter",
"filament_self_index",
"filament_extruder_variant"
};
for (const std::string &key : s_material_identity_keys) {
base_keys_to_include.insert(key);
mask_exempt_keys.insert(key);
}
// 2. Published plate / bed geometry keys (wipe tower positioning)
static const std::vector<std::string> s_plate_geometry_keys = {
"wipe_tower_x",
"wipe_tower_y",
"wipe_tower_rotation_angle"
};
for (const std::string &key : s_plate_geometry_keys) {
base_keys_to_include.insert(key);
mask_exempt_keys.insert(key);
}
// 3. Process and printer published keys. Printer per-extruder keys carry a "#N" variant
// (e.g. retraction_length#2): mask the base to the author's extruder index so a partial
// publish does not serialize every extruder's value (same slot-masking as the material side).
const std::set<std::string> &printer_keys = publishable_printer_keys();
for (const std::string &key : published_keys) {
const std::string base_key = publish_base_key(key);
if (base_key.empty())
continue;
base_keys_to_include.insert(base_key);
if (printer_keys.count(base_key) != 0) {
const int variant_idx = publish_variant_index(key, base_key);
if (variant_idx >= 0)
slot_mask_map[base_key].insert(variant_idx);
else
mask_exempt_keys.insert(base_key); // bare printer key or malformed variant: whole vector
} else {
mask_exempt_keys.insert(base_key); // process key: whole vector
}
}
// 4. Material-specific published keys. Both partial (entry.keys) and full-publish
// (entry.full_keys) entries mask to the author's slot on export (see the copy loop below);
// slot-less entries (hand-crafted files) stay unmasked (whole vector).
for (const PublishedMaterialEntry &entry : material_keys) {
for (const std::string &key : entry.keys) {
const std::string base_key = publish_base_key(key);
if (base_key.empty())
continue;
base_keys_to_include.insert(base_key);
if (entry.slot >= 0)
slot_mask_map[base_key].insert(entry.slot);
}
for (const std::string &key : entry.full_keys) {
const std::string base_key = publish_base_key(key);
if (base_key.empty())
continue;
base_keys_to_include.insert(base_key);
if (entry.slot >= 0)
slot_mask_map[base_key].insert(entry.slot);
}
}
// Masking restores every non-published slot of a vector option with the option default, so
// a partial publish does not leak unrelated slot data. can_mask_slots reports whether a key
// is maskable at all (vector option plus a registered default of the same type); an
// unmaskable key is dropped from the payload entirely instead of shipping the author's
// whole vector.
auto can_mask_slots = [](const ConfigOption &opt, const ConfigOptionDef *def) -> bool {
if (def == nullptr || !def->default_value || def->default_value->type() != opt.type())
return false;
const auto *vec = dynamic_cast<const ConfigOptionVectorBase *>(&opt);
const auto *default_vec = dynamic_cast<const ConfigOptionVectorBase *>(def->default_value.get());
return vec != nullptr && vec->size() > 0 && default_vec != nullptr && !default_vec->empty();
};
auto mask_slots = [](ConfigOption &opt, const ConfigOptionDef *def, const std::set<int> &keep_slots) {
auto *vec = dynamic_cast<ConfigOptionVectorBase*>(&opt);
for (size_t idx = 0; idx < vec->size(); ++idx)
if (keep_slots.count(static_cast<int>(idx)) == 0)
vec->set_at(def->default_value.get(), idx, 0);
};
// Copy the selected options from full_config into the filtered config.
for (const std::string &key : base_keys_to_include) {
const ConfigOption *opt = full_config.option(key);
if (opt == nullptr)
continue;
const auto mask_it = slot_mask_map.find(key);
const bool needs_masking = mask_exempt_keys.count(key) == 0 && mask_it != slot_mask_map.end() && !mask_it->second.empty();
if (needs_masking && !can_mask_slots(*opt, print_config_def.get(key))) {
BOOST_LOG_TRIVIAL(warning) << "publish: dropping unmaskable key \"" << key
<< "\" from the published payload (no usable option default)";
continue;
}
ConfigOption *cloned = opt->clone();
if (needs_masking)
mask_slots(*cloned, print_config_def.get(key), mask_it->second);
filtered.set_key_value(key, cloned);
}
return filtered;
}
} // namespace Slic3r
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include <set>
#include <string>
#include <vector>
namespace Slic3r {
class PresetBundle;
// Strip a trailing "#N" variant suffix ("retraction_length#2" -> "retraction_length").
std::string publish_base_key(const std::string &key);
// Structural keys that are never applied onto the receiver's presets when loading a published
// 3MF (single source of truth for the denylist); applying them would rewrite the user's preset
// inheritance/structure. filament_ids is still exported via the identity list (3MF validation
// needs it) - exported, never applied.
const std::set<std::string>& publish_structural_keys();
// The mixed-color filament project keys (parallel per-slot arrays, see PresetBundle's
// s_project_options). Import applies them into project_config, not a filament preset.
const std::set<std::string>& publish_mixed_keys();
// One row of the printer tab's "Retraction" / "Z-Hop" optgroups (config key + tab icon id).
struct PublishablePrinterOption {
const char *key; // config key, e.g. "retraction_length"
const char *icon; // tab icon id, e.g. "printer_extruder_retraction#length"
};
// The printer tab's "Retraction" / "Z-Hop" optgroup options, in tab order.
const std::vector<PublishablePrinterOption>& publishable_printer_retraction_options();
const std::vector<PublishablePrinterOption>& publishable_printer_z_hop_options();
// Union of the two optgroup option lists; printer keys apply on import only if their base
// key is in this allowlist.
const std::set<std::string>& publishable_printer_keys();
// Union of setting keys differing from the base/system preset across the current print,
// printer and filament presets (feeds the Publish dialog's pre-check).
std::vector<std::string> collect_dirty_settings_keys(const PresetBundle& bundle);
// Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
// The identity fields drive the created copy's naming and grouping on Full entries, the
// notification labels, and the partial type gate (publish_type) is the author's explicit
// opt-in for requiring a material type.
struct PublishedMaterialEntry {
std::string filament_type; // material family, e.g. "PLA" (may be empty)
std::string filament_vendor; // e.g. "Generic", "Bambu" (may be empty)
std::string filament_id; // stable material id, e.g. "GFL99" (may be empty)
// Unique preset id of the author's slot preset (e.g. Orca Filament Library "setting_id").
// Not matched against the receiver's library; carried so identical Full entries within one
// load share one created instance (within-load dedup key).
std::string setting_id;
// Canonical name of the author's slot preset (e.g. "Generic PLA @System"). On Full import
// it names the created copy after its "@variant" tail is stripped; never matched against
// the receiver's library.
std::string preset_name;
// 0-based author filament slot; -1 (hand-crafted files) is skipped.
int slot{-1};
std::vector<std::string> keys;
// "Full Publish": the whole filament preset (full_keys) is published. On the receiver Full
// Publish always creates a standalone parentless copy (libslic3r's "Detach from parent"),
// universally compatible and project-embedded only - never written to the user's library.
// Identical Full entries within one load share one created instance (within-load dedup).
bool full{false};
// All non-structural filament keys of the author's slot preset; values travel in the file
// config, masked to the author's slot index.
std::vector<std::string> full_keys;
// Vendor-agnostic (MaterialType) filament type the author requires for this slot; on a
// partial entry's mismatch the slot is replaced with a same-type filament. Full entries
// consult no gate.
bool publish_type{false};
std::string publish_type_value;
// Required filament colour, applied on load regardless of the type match.
bool publish_color{false};
std::string color;
// Import-side only, never serialized: the authored slot sits past the receiver's physical
// capacity, so the entry is appended as an empty mixed-filament placeholder (virtual tail
// slot; the GUI flags it for the user to assign components).
bool mixed_placeholder{false};
};
// "PLA High Speed" -> "PLA" (strip a space modifier); dash types like "PA-CF" are kept intact.
std::string normalize_filament_type(const std::string& type);
class DynamicPrintConfig;
// Clear the compatibility lists/conditions on a filament config so it is universally
// compatible once detached (empty lists + empty conditions = compatible with everything).
void make_publish_universal(DynamicPrintConfig &config);
// Naming base for a detached published-material copy: "Generic PLA @System" -> "Generic PLA"
// (truncate/right-trim at the first '@' tail). Empty result means "fall back to identity".
std::string publish_material_base_name(const std::string &preset_name);
// Minimal DynamicPrintConfig for a published 3MF export: only the selected published keys,
// material keys, identity fields and plate geometry keys.
DynamicPrintConfig filter_published_config(
const DynamicPrintConfig &full_config,
const std::vector<std::string> &published_keys,
const std::vector<PublishedMaterialEntry> &material_keys);
}
+4
View File
@@ -97,6 +97,8 @@ set(SLIC3R_GUI_SOURCES
GUI/CloneDialog.hpp
GUI/ConfigManipulation.cpp
GUI/ConfigManipulation.hpp
GUI/ConfigValueFormatter.cpp
GUI/ConfigValueFormatter.hpp
GUI/ConfigWizard.cpp
GUI/ConfigWizard.hpp
GUI/ConfigWizard_private.hpp
@@ -453,6 +455,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Project.hpp
GUI/PublishDialog.cpp
GUI/PublishDialog.hpp
GUI/PublishSettingsDialog.cpp
GUI/PublishSettingsDialog.hpp
GUI/PurgeModeDialog.cpp
GUI/PurgeModeDialog.hpp
GUI/RammingChart.cpp
+245
View File
@@ -0,0 +1,245 @@
#include "ConfigValueFormatter.hpp"
#include <algorithm>
#include <cstdlib>
#include <string>
#include <vector>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
#include "libslic3r/Config.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "I18N.hpp"
#include "GUI.hpp"
#include "Field.hpp"
namespace Slic3r {
namespace GUI {
std::string get_pure_opt_key(const std::string& opt_key)
{
std::string pure_key = opt_key;
const int pos = pure_key.find("#");
if (pos > 0)
boost::erase_tail(pure_key, pure_key.size() - pos);
return pure_key;
}
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill, int idx)
{
const ConfigOptionDef& def = config.def()->options.at(opt_key);
const std::vector<std::string>& names = def.enum_labels;//ConfigOptionEnum<T>::get_enum_names();
int val = 0;
if (idx >= 0)
val = dynamic_cast<const ConfigOptionInts*>(config.option(opt_key))->get_at(idx);
else
val = config.option(opt_key)->getInt();
// Each infill doesn't use all list of infill declared in PrintConfig.hpp.
// So we should "convert" val to the correct one
if (is_infill) {
for (auto key_val : *def.enum_keys_map)
if (int(key_val.second) == val) {
auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first);
if (it == def.enum_values.end())
return "";
return from_u8(_utf8(names[it - def.enum_values.begin()]));
}
return _L("Undefined");
}
return from_u8(_utf8(names[val]));
}
wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config)
{
const std::string pure_key = get_pure_opt_key(opt_key);
auto option = config.option(pure_key);
if (!option || option->is_nil())
return _L("N/A");
const ConfigOptionDef* opt = config.def()->get(pure_key);
return opt->full_label.empty() ? opt->label : opt->full_label;
}
wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config)
{
int orig_opt_idx = -1;
int opt_idx = -1;
int pos = opt_key.find("#");
std::string temp_str = opt_key;
if (pos > 0) {
boost::erase_head(temp_str, pos + 1);
orig_opt_idx = std::atoi(temp_str.c_str());
}
opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0;
const std::string pure_key = get_pure_opt_key(opt_key);
auto option = config.option(pure_key);
if (!option) {
return _L("N/A");
}
auto opt_vector = dynamic_cast<const ConfigOptionVectorBase *>(option);
if ((option->is_scalar() && option->is_nil()) ||
(option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)))
return _L("N/A");
wxString out;
const ConfigOptionDef* opt = config.def()->get(pure_key);
bool is_nullable = opt->nullable;
switch (opt->type) {
case coInt:
return from_u8((boost::format("%1%") % config.opt_int(pure_key)).str());
case coInts: {
if (is_nullable) {
auto values = config.opt<ConfigOptionIntsNullable>(pure_key);
if (opt_idx < values->size())
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
}
else {
auto values = config.opt<ConfigOptionInts>(pure_key);
if (orig_opt_idx >= 0 && orig_opt_idx < values->size()) {
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
}
else {
std::string value_str;
for (int i = 0; i < values->size(); i++) {
value_str += std::to_string(values->get_at(i));
if (i != values->size() - 1) {
value_str += ",";
}
}
return from_u8(value_str);
}
}
return _L("Undefined");
}
case coBool:
return config.opt_bool(pure_key) ? "true" : "false";
case coBools: {
if (is_nullable) {
auto values = config.opt<ConfigOptionBoolsNullable>(pure_key);
if (opt_idx < values->size())
return values->get_at(opt_idx) ? "true" : "false";
}
else {
auto values = config.opt<ConfigOptionBools>(pure_key);
if (opt_idx < values->size())
return values->get_at(opt_idx) ? "true" : "false";
}
return _L("Undefined");
}
case coPercent:
return from_u8((boost::format("%1%%%") % int(config.optptr(pure_key)->getFloat())).str());
case coPercents: {
if (is_nullable) {
auto values = config.opt<ConfigOptionPercentsNullable>(pure_key);
if (opt_idx < values->size())
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
}
else {
auto values = config.opt<ConfigOptionPercents>(pure_key);
if (opt_idx < values->size())
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
}
return _L("Undefined");
}
case coFloat:
return double_to_string(config.opt_float(pure_key));
case coFloats: {
if (is_nullable) {
auto values = config.opt<ConfigOptionFloatsNullable>(pure_key);
if (opt_idx < values->size())
return double_to_string(values->get_at(opt_idx));
}
else {
auto values = config.opt<ConfigOptionFloats>(pure_key);
if (values && opt_idx < values->size())
return double_to_string(values->get_at(opt_idx));
}
return _L("Undefined");
}
case coString:
return from_u8(config.opt_string(pure_key));
case coStrings: {
const ConfigOptionStrings* strings = config.opt<ConfigOptionStrings>(pure_key);
if (strings) {
if (pure_key == "compatible_printers" || pure_key == "compatible_prints") {
if (strings->empty())
return _L("All");
for (size_t id = 0; id < strings->size(); id++)
out += from_u8(strings->get_at(id)) + "\n";
out.RemoveLast(1);
return out;
}
if (!strings->empty() && opt_idx < strings->values.size())
return from_u8(strings->get_at(opt_idx));
}
break;
}
case coFloatOrPercent: {
const ConfigOptionFloatOrPercent* opt = config.opt<ConfigOptionFloatOrPercent>(pure_key);
if (opt)
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
return out;
}
case coEnum: {
return get_string_from_enum(pure_key, config,
pure_key == "top_surface_pattern" ||
pure_key == "bottom_surface_pattern" ||
pure_key == "internal_solid_infill_pattern" ||
pure_key == "sparse_infill_pattern" ||
pure_key == "ironing_pattern" ||
pure_key == "support_ironing_pattern" ||
pure_key == "support_pattern" ||
pure_key == "support_interface_pattern")
;
}
case coEnums: {
return get_string_from_enum(pure_key, config,
pure_key == "top_surface_pattern" ||
pure_key == "bottom_surface_pattern" ||
pure_key == "internal_solid_infill_pattern" ||
pure_key == "sparse_infill_pattern" ||
pure_key == "ironing_pattern" ||
pure_key == "support_ironing_pattern" ||
pure_key == "support_pattern" ||
pure_key == "support_interface_pattern"
, opt_idx);
}
case coPoint: {
Vec2d val = config.opt<ConfigOptionPoint>(pure_key)->value;
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
}
case coPoints: {
//BBS: add bed_exclude_area
if (pure_key == "printable_area" || pure_key == "thumbnails") {
ConfigOptionPoints points = *config.option<ConfigOptionPoints>(pure_key);
//BuildVolume build_volume = {points.values, 0.};
return get_thumbnails_string(points.values);
}
else if (pure_key == "bed_exclude_area") {
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
}
else if (pure_key == "head_wrap_detect_zone") {
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
}
else if (pure_key == "wrapping_exclude_area") {
return get_thumbnails_string(config.option<ConfigOptionPoints>(pure_key)->values);
}
Vec2d val = config.opt<ConfigOptionPoints>(pure_key)->get_at(opt_idx);
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
}
default:
break;
}
return out;
}
} // namespace GUI
} // namespace Slic3r
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <string>
#include <wx/string.h>
namespace Slic3r {
class DynamicPrintConfig;
namespace GUI {
// Human-readable value of opt_key (may carry a "#<index>" suffix) in config.
wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig& config);
// Full label of opt_key; "N/A" when the option is not set.
wxString get_full_label(const std::string& opt_key, const DynamicPrintConfig& config);
// Strip the "#<index>" suffix (if any) from the option key.
std::string get_pure_opt_key(const std::string& opt_key);
// Localized label of the currently selected value of an enum option.
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1);
} // namespace GUI
} // namespace Slic3r
+425 -1
View File
@@ -1,16 +1,66 @@
#include <wx/dcmemory.h>
#include <wx/dcgraph.h>
#include <wx/graphics.h>
#include <wx/settings.h>
#include <wx/window.h>
#include <algorithm>
#include <cmath>
#include <map>
#include <numeric>
#include <string>
#include <tuple>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
#include "I18N.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/StateColor.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r { namespace GUI {
// Barycentric utilities for a ternary (triangle) ratio picker.
double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c)
{
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
}
bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
{
double total = tri_signed_area2(v0, v1, v2);
if (std::abs(total) < 1e-9) return false;
double s0 = tri_signed_area2(p, v1, v2) / total;
double s1 = tri_signed_area2(v0, p, v2) / total;
double s2 = 1.0 - s0 - s1;
return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001;
}
void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
double& w0, double& w1, double& w2)
{
double total = std::abs(tri_signed_area2(v0, v1, v2));
if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; }
w0 = std::abs(tri_signed_area2(p, v1, v2)) / total;
w1 = std::abs(tri_signed_area2(v0, p, v2)) / total;
w2 = 1.0 - w0 - w1;
w0 = std::clamp(w0, 0.0, 1.0);
w1 = std::clamp(w1, 0.0, 1.0);
w2 = std::clamp(w2, 0.0, 1.0);
double s = w0 + w1 + w2;
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
}
TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
{
double w0, w1, w2;
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
return {w0 * v0.x + w1 * v1.x + w2 * v2.x,
w0 * v0.y + w1 * v1.y + w2 * v2.y};
}
void fill_gradient_rect_east(wxDC& dc, const wxRect& rect, const wxColour& from, const wxColour& to)
{
if (rect.width <= 0 || rect.height <= 0) return;
@@ -73,7 +123,7 @@ std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
// Resolve the curve a gradient slot is sampled with, mirroring the slicer's fallback in
// ToolOrdering: a custom curve wins, otherwise a straight line between gradient_range's
// endpoints, otherwise the 0.10 -> 0.90 default.
static Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot)
{
const auto* curve_opt = cfg.option<ConfigOptionStrings>("filament_mixed_gradient_curve");
if (curve_opt && slot < curve_opt->values.size() && !curve_opt->values[slot].empty()) {
@@ -449,4 +499,378 @@ void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
}
}
namespace {
// Layout ratios of the gradient plot rect, copied from GradientCurveEditor so the read-only
// preview and the interactive editor stay pixel-identical. Plot rect is square 1:1; the
// right/bottom margins host the axis arrows and labels.
constexpr double kPlotLeftRatio = 0.0316;
constexpr double kPlotRightRatio = 0.6766;
constexpr double kPlotTopRatio = 0.1529;
constexpr double kPlotBottomRatio = 0.8474;
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling)
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
constexpr float kBgSimilarThreshold = 15.0f;
constexpr int kOutlineExtraDip = 2;
constexpr double kTriangleMarginDip = 20.0;
// Quadratic blend that never goes out of gamut, matching MixedFilamentDialog::blend_colors.
wxColour lerp_blend(const wxColour& a, const wxColour& b, double ratio_a)
{
unsigned char r, g, bl;
Slic3r::filament_mixer_lerp(a.Red(), a.Green(), a.Blue(),
b.Red(), b.Green(), b.Blue(),
static_cast<float>(1.0 - ratio_a), &r, &g, &bl);
return wxColour(r, g, bl);
}
// DIP conversion for these free functions: unlike the wxWindow member FromDIP, it needs the
// window parameter explicitly; nullptr picks the app's default DPI like the Publish dialog does.
int dip_px(int v) { return wxWindow::FromDIP(v, nullptr); }
} // namespace
wxRect mixed_gradient_plot_rect(const wxSize& sz)
{
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
const int side = std::max(1, std::min(x2 - x, y2 - y));
return wxRect(x, y, side, side);
}
void draw_mixed_gradient_plot(wxDC& raw_dc, const wxSize& canvas,
const std::vector<MixedGradientCurve>& curves,
const std::vector<wxPoint2DDouble>& anchors,
const MixedGradientTheme& theme)
{
// Draw into an internal opaque buffer so wxGCDC text/curves anti-alias against a solid
// background (never a transparent one), then blit the finished image onto the caller's
// buffered paint DC. wxGCDC cannot wrap a generic wxDC&, so the buffer is always a
// wxMemoryDC -- the one type wxGCDC accepts on every platform.
if (canvas.x <= 0 || canvas.y <= 0)
return;
const wxRect rc = mixed_gradient_plot_rect(canvas);
if (rc.width <= 0 || rc.height <= 0)
return;
wxBitmap buf(canvas);
wxMemoryDC memdc(buf);
memdc.SetBackground(wxBrush(theme.background));
memdc.Clear();
wxGCDC dc(memdc);
wxGraphicsContext* gc = dc.GetGraphicsContext();
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
dc.SetPen(wxPen(theme.grid, 1));
for (int i = 0; i <= kGridDivisions; ++i) {
const int x = rc.x + rc.width * i / kGridDivisions;
const int y = rc.y + rc.height * i / kGridDivisions;
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
}
// Set the label font first so text width measurements drive arrow / label placement.
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
dc.SetFont(label_font);
const wxString axis_y_title = _L("Material Ratio");
const wxString axis_x_title = _L("Model Height");
const wxString pct_text = wxT("100%");
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
wxFont strong_font = label_font;
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
dc.SetFont(strong_font);
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
dc.SetFont(label_font);
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
const int arrow_half = dip_px(kAxisArrowHalf);
const int arrow_len = dip_px(kAxisArrowLen);
dc.SetPen(wxPen(theme.axis, kStrokeAxis));
dc.SetBrush(wxBrush(theme.axis));
const int y_axis_x = rc.x;
const int y_title_pct_gap = dip_px(1);
const int y_title_bottom_pad = dip_px(2);
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
const int y_arrow_tip_y = y_title_y;
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
{
wxPoint tri[3] = {
wxPoint(y_axis_x, y_arrow_tip_y),
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
};
dc.DrawPolygon(3, tri);
}
const int x_axis_y = rc.y + rc.height;
const int x_label_gap = dip_px(4);
const int x_edge_pad = dip_px(6);
const int x_arrow_ideal = rc.x + rc.width + dip_px(10);
const int x_arrow_max = canvas.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len, std::min(x_arrow_ideal, x_arrow_max));
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
const int x_title_x = x_arrow_tip_x + x_label_gap;
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
{
wxPoint tri[3] = {
wxPoint(x_arrow_tip_x, x_axis_y),
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
};
dc.DrawPolygon(3, tri);
}
// Labels: "Material Ratio" and the leading "100%" share the same left x; the trailing
// "Model Height" follows the X-axis arrow tip (already clamped to make room).
const int label_left_x = y_axis_x + dip_px(10);
dc.SetTextForeground(theme.label);
dc.DrawText(axis_y_title, label_left_x, y_title_y);
dc.SetFont(strong_font);
dc.SetTextForeground(theme.label_strong);
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
dc.SetFont(label_font);
dc.SetTextForeground(theme.label);
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
if (!gc)
return;
// Outline only when the curve colour is perceptually close to the background; otherwise the
// plain filament colour reads fine and the extra stroke would look heavy.
auto needs_outline = [&](const wxColour& c) {
return calc_color_distance(c, theme.background) < kBgSimilarThreshold;
};
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which
// forwards it here while keeping its own cached state in sync for later dc drawing.
auto draw_polyline = [&](const MixedGradientCurve& curve) {
if (curve.points.size() < 2)
return;
dc.SetPen(wxPen(curve.colour, dip_px(curve.stroke_dip)));
gc->StrokeLines(curve.points.size(), curve.points.data());
};
for (const MixedGradientCurve& curve : curves) {
if (needs_outline(curve.colour))
draw_polyline({curve.points, theme.outline, curve.stroke_dip + kOutlineExtraDip});
draw_polyline(curve);
}
// Control points: hollow circle with axis-colour border, theme-aware fill, drawn with a
// sub-pixel centre so the ring stays centred on the curve.
if (!anchors.empty()) {
const double r = dip_px(kPointRadius);
dc.SetPen(wxPen(theme.axis, 1));
dc.SetBrush(wxBrush(theme.point_fill));
for (const wxPoint2DDouble& p : anchors)
gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2);
}
memdc.SelectObject(wxNullBitmap);
raw_dc.DrawBitmap(buf, 0, 0);
}
void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first,
const wxColour& second, double second_fraction)
{
if (rect.width <= 0 || rect.height <= 0)
return;
for (int x = 0; x < rect.width; ++x) {
const double t = rect.width > 1 ? double(x) / rect.width : 0.0;
const wxColour c = lerp_blend(first, second, 1.0 - t);
dc.SetPen(wxPen(c));
dc.DrawLine(rect.x + x, rect.y, rect.x + x, rect.y + rect.height);
}
// Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over
// blended filament colour, so it has to keep its contrast against data rather than chrome.
const int div_x = rect.x + static_cast<int>(second_fraction * rect.width);
dc.SetPen(wxPen(wxColour(80, 80, 80), dip_px(4)));
dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height);
dc.SetPen(wxPen(*wxWHITE, dip_px(2)));
dc.DrawLine(div_x, rect.y, div_x, rect.y + rect.height);
}
void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& colours,
const std::vector<double>& shares)
{
const size_t n = std::min(colours.size(), shares.size());
if (n == 0 || rect.width <= 0 || rect.height <= 0)
return;
std::vector<double> norm = shares;
double total = 0.0;
for (double s : norm)
total += s;
if (total <= 0.0) {
norm.assign(n, 1.0 / n);
total = 1.0;
}
auto share_to_px = [&](double share_sum) { return rect.x + int(std::lround(share_sum / total * double(rect.width))); };
int x0 = rect.x;
std::vector<wxRect> segs(n);
for (size_t i = 0; i < n; ++i) {
int x1 = rect.x + rect.width;
if (i + 1 < n)
x1 = share_to_px(std::accumulate(norm.begin(), norm.begin() + i + 1, 0.0));
segs[i] = wxRect(x0, rect.y, std::max(1, x1 - x0), rect.height);
x0 = segs[i].GetRight() + 1;
}
for (size_t i = 0; i < n; ++i) {
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(colours[i]));
dc.DrawRectangle(segs[i]);
}
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#ACACAC")), 1));
dc.DrawRectangle(rect);
}
namespace {
struct TriCacheKey
{
int w, h;
int c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b;
int bg_r, bg_g, bg_b, ol_r, ol_g, ol_b;
bool operator<(const TriCacheKey& o) const
{
return std::tie(w, h, c0r, c0g, c0b, c1r, c1g, c1b, c2r, c2g, c2b, bg_r, bg_g, bg_b, ol_r, ol_g, ol_b) <
std::tie(o.w, o.h, o.c0r, o.c0g, o.c0b, o.c1r, o.c1g, o.c1b, o.c2r, o.c2g, o.c2b, o.bg_r, o.bg_g, o.bg_b, o.ol_r, o.ol_g, o.ol_b);
}
};
std::map<TriCacheKey, wxBitmap>& tri_cache()
{
static std::map<TriCacheKey, wxBitmap> cache;
return cache;
}
} // namespace
std::array<TriPoint, 3> mixed_triangle_vertices(const wxSize& size, double margin_dip)
{
const double pw = size.GetWidth(), ph = size.GetHeight();
const double margin = dip_px(int(margin_dip));
const double avail = std::min(pw, ph) - 2.0 * margin;
const double side = avail;
const double tri_h = side * std::sqrt(3.0) / 2.0;
const double cx = pw / 2.0;
const double top_y = (ph - tri_h) / 2.0;
return {{{cx, top_y}, {cx - side / 2.0, top_y + tri_h}, {cx + side / 2.0, top_y + tri_h}}};
}
void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array<wxColour, 3>& colours,
const std::array<double, 3>& weights, const MixedTriangleTheme& theme)
{
if (size.GetWidth() <= 0 || size.GetHeight() <= 0)
return;
const std::array<TriPoint, 3> v = mixed_triangle_vertices(size, kTriangleMarginDip);
dc.SetBrush(wxBrush(theme.background));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight());
const wxColour& c0 = colours[0];
const wxColour& c1 = colours[1];
const wxColour& c2 = colours[2];
const TriCacheKey key{size.GetWidth(), size.GetHeight(),
c0.Red(), c0.Green(), c0.Blue(),
c1.Red(), c1.Green(), c1.Blue(),
c2.Red(), c2.Green(), c2.Blue(),
theme.background.Red(), theme.background.Green(), theme.background.Blue(),
theme.outline.Red(), theme.outline.Green(), theme.outline.Blue()};
wxBitmap& bmp = tri_cache()[key];
if (!bmp.IsOk()) {
bmp = wxBitmap(size.GetWidth(), size.GetHeight(), 24);
wxMemoryDC mdc(bmp);
mdc.SetBrush(wxBrush(theme.background));
mdc.SetPen(*wxTRANSPARENT_PEN);
mdc.DrawRectangle(0, 0, size.GetWidth(), size.GetHeight());
const int min_y = int(std::min({v[0].y, v[1].y, v[2].y}));
const int max_y = int(std::max({v[0].y, v[1].y, v[2].y}));
const int min_x = int(std::min({v[0].x, v[1].x, v[2].x}));
const int max_x = int(std::max({v[0].x, v[1].x, v[2].x}));
for (int py = min_y; py <= max_y; ++py) {
for (int px = min_x; px <= max_x; ++px) {
const TriPoint p = {double(px), double(py)};
if (!tri_contains(p, v[0], v[1], v[2]))
continue;
double w0, w1, w2;
tri_barycentric(p, v[0], v[1], v[2], w0, w1, w2);
unsigned char mr, mg, mb;
if (w0 + w1 > 1e-6) {
float t01 = float(w1 / (w0 + w1));
Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(), c1.Red(), c1.Green(), c1.Blue(), t01, &mr, &mg, &mb);
Slic3r::filament_mixer_lerp(mr, mg, mb, c2.Red(), c2.Green(), c2.Blue(), float(w2), &mr, &mg, &mb);
} else {
mr = c2.Red(); mg = c2.Green(); mb = c2.Blue();
}
mdc.SetPen(wxPen(wxColour(mr, mg, mb)));
mdc.DrawPoint(px, py);
}
}
mdc.SetPen(wxPen(theme.outline, 1));
mdc.SetBrush(*wxTRANSPARENT_BRUSH);
const wxPoint pts[3] = {{int(v[0].x), int(v[0].y)}, {int(v[1].x), int(v[1].y)}, {int(v[2].x), int(v[2].y)}};
mdc.DrawPolygon(3, pts);
mdc.SelectObject(wxNullBitmap);
// Keep the cache from growing without bound across DPI/size changes.
if (tri_cache().size() > 6) {
auto& cache = tri_cache();
cache.erase(cache.begin());
}
}
dc.DrawBitmap(bmp, 0, 0);
// Published-ratio marker (read-only twin of the editor's drag handle).
const double w0 = weights[0], w1 = weights[1], w2 = weights[2];
const int hx = int(w0 * v[0].x + w1 * v[1].x + w2 * v[2].x);
const int hy = int(w0 * v[0].y + w1 * v[1].y + w2 * v[2].y);
dc.SetBrush(*wxWHITE_BRUSH);
dc.SetPen(wxPen(theme.ring, dip_px(2)));
dc.DrawCircle(hx, hy, dip_px(5));
}
void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array<double, 3>& weights,
const MixedTriangleTheme& theme)
{
const std::array<TriPoint, 3> v = mixed_triangle_vertices(size, kTriangleMarginDip);
dc.SetFont(::Label::Body_12);
dc.SetTextForeground(theme.label);
// "Ratio" title, sitting above the top vertex.
const wxString title = _L("Ratio");
dc.DrawText(title, dip_px(2), std::max(0, int(v[0].y - dc.GetTextExtent(title).GetHeight() - dip_px(4))));
for (int i = 0; i < 3; ++i) {
const wxString text = wxString::Format("%d%%", int(std::lround(weights[i] * 100.0)));
const wxSize tsz = dc.GetTextExtent(text);
int lx = int(v[i].x - tsz.GetWidth() / 2.0);
int ly = (i == 0) ? int(v[i].y - tsz.GetHeight() - dip_px(4)) : int(v[i].y + dip_px(3));
ly = std::clamp(ly, 0, size.GetHeight() - tsz.GetHeight());
lx = std::clamp(lx, 0, size.GetWidth() - tsz.GetWidth());
dc.DrawText(text, lx, ly);
}
}
}} // namespace Slic3r::GUI
+94
View File
@@ -5,6 +5,9 @@
#include <wx/colour.h>
#include <wx/dc.h>
#include <wx/gdicmn.h>
#include <wx/geometry.h>
#include <wx/graphics.h>
#include <array>
#include <vector>
// Orca: forward-declare so the header is self-contained outside libslic3r_gui's
@@ -13,6 +16,16 @@ namespace Slic3r { class DynamicPrintConfig; struct GradientCurve; }
namespace Slic3r { namespace GUI {
// Barycentric utilities for a ternary (triangle) ratio picker, shared by the mixed-filament
// editor and the Publish dialog's read-only definition preview.
struct TriPoint { double x, y; };
double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c);
bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2);
void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
double& w0, double& w1, double& w2);
TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2);
// Fills a rect with a west->east linear gradient by drawing solid 1px columns.
// Use instead of wxDC::GradientFillLinear, whose CoreGraphics (CGShading) backend
// fails to render on some macOS builds; solid fills are unaffected.
@@ -51,6 +64,12 @@ std::vector<wxColour> sample_gradient_ramp(const wxColour& first,
// destination's height in pixels.
std::vector<wxColour> mixed_gradient_ramp(const Slic3r::DynamicPrintConfig& cfg, size_t slot, int steps);
// Resolve the curve a gradient slot is sampled with: the custom curve wins when it has at
// least two points, otherwise a straight line between gradient_range's endpoints, otherwise
// the 0.10 -> 0.90 default. Mirrors the slicer's ToolOrdering fallback so every preview
// agrees with what gets sliced. Always returns a two-point curve.
Slic3r::GradientCurve mixed_gradient_curve(const Slic3r::DynamicPrintConfig& cfg, size_t slot);
// Fill rect with a ramp, ramp.front() along the bottom edge.
void fill_gradient_ramp_rect(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& ramp);
@@ -63,6 +82,81 @@ wxBitmap create_gradient_ramp_bitmap(const std::vector<wxColour>& ramp, const wx
void recompute_mixed_slot_colors(std::vector<wxColour>& colors,
const Slic3r::DynamicPrintConfig& cfg);
// --- Gradient plot (shared by GradientCurveEditor and the Publish dialog's read-only
// preview). The plot is a square 1:1 rect laid out with the editor's ratios so both
// render identically; curves are drawn as sub-pixel anti-aliased polylines through
// wxGCDC so they never quantize to whole pixels.
// One curve of the plot: screen-space sub-pixel points already mapped into the plot
// rect, the stroke colour and the stroke width in DIP.
struct MixedGradientCurve
{
std::vector<wxPoint2DDouble> points;
wxColour colour;
int stroke_dip;
};
// Theme tokens, resolved by the caller through StateColor::darkModeColorFor.
struct MixedGradientTheme
{
wxColour background; // for near-background outline detection
wxColour grid; // grid line
wxColour axis; // axis + arrow fill
wxColour label; // "Material Ratio" / "Model Height"
wxColour label_strong; // "100%"
wxColour outline; // near-background curve lift
wxColour point_fill; // anchor fill
};
// Square 1:1 plot rect inside `canvas`, using the editor's plot ratios.
wxRect mixed_gradient_plot_rect(const wxSize& canvas);
// Draw the whole plot (grid, axes + arrowheads, axis labels, each curve with an optional
// near-background outline, and anchor circles). `anchors` are empty when the caller has
// none to show. `dc` is the caller's buffered paint DC; a wxGCDC is created inside so the
// geometry gets anti-aliased.
void draw_mixed_gradient_plot(wxDC& dc, const wxSize& canvas,
const std::vector<MixedGradientCurve>& curves,
const std::vector<wxPoint2DDouble>& anchors,
const MixedGradientTheme& theme);
// --- Ratio bar (2-component continuous blend + divider, matching MixedFilamentDialog).
// Colours blend first->second across the rect; the divider marks `second_fraction` of the
// rect's width (the second component's share, 0..1).
void draw_mixed_ratio_blend_bar(wxDC& dc, const wxRect& rect, const wxColour& first,
const wxColour& second, double second_fraction);
// Fallback ratio bar for N>2 non-gradient slots: one solid segment per component,
// widths proportional to shares. Label text (the "NN%" inside wide-enough segments) is
// the caller's concern.
void draw_mixed_ratio_segments(wxDC& dc, const wxRect& rect, const std::vector<wxColour>& colours,
const std::vector<double>& shares);
// --- Triangle picker (3-component), shared by MixedFilamentDialog and the Publish preview.
struct MixedTriangleTheme
{
wxColour background;
wxColour outline; // triangle border
wxColour ring; // drag-handle ring
wxColour label; // "Ratio" title + per-vertex labels
};
// The three vertices of the read-only/miniature triangle inside a `size` square panel,
// with `margin_dip` inset. Order: top, bottom-left, bottom-right.
std::array<TriPoint, 3> mixed_triangle_vertices(const wxSize& size, double margin_dip = 20.0);
// Draw background, the cached barycentric fill, the outline and the drag-handle marker.
// `weights` are the three barycentric shares (sum 1). The "Ratio" title and per-vertex
// percentage labels are drawn by the caller so the interactive editor can keep its own
// live child labels while the read-only preview draws them as text.
void draw_mixed_triangle_picker(wxDC& dc, const wxSize& size, const std::array<wxColour, 3>& colours,
const std::array<double, 3>& weights, const MixedTriangleTheme& theme);
// Draw the "Ratio" title plus one "NN%" label per vertex (used by the read-only preview;
// the interactive editor positions its own live labels instead).
void draw_mixed_triangle_labels(wxDC& dc, const wxSize& size, const std::array<double, 3>& weights,
const MixedTriangleTheme& theme);
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_FilamentBitmapUtils_hpp_
+45 -186
View File
@@ -1,4 +1,5 @@
#include "GradientCurveEditor.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
#include "I18N.hpp"
@@ -19,23 +20,13 @@ namespace GUI {
wxDEFINE_EVENT(wxEVT_GRADIENT_CURVE_CHANGED, wxCommandEvent);
namespace {
// Layout ratios of the plot rect within the widget, taken from a 214 x 180 px reference drawing.
// Plot rect occupies the upper-left region; right + bottom margins host axis arrows / labels.
constexpr double kPlotLeftRatio = 0.0316;
constexpr double kPlotRightRatio = 0.6766;
constexpr double kPlotTopRatio = 0.1529;
constexpr double kPlotBottomRatio = 0.8474;
constexpr int kGridDivisions = 9; // 10 grid lines including the outer borders.
// Hit / stroke (DIP).
// Hit / stroke (DIP). The plot-rect ratios, grid divisions, axis/arrow geometry and the
// near-background outline threshold now live in FilamentBitmapUtils so the read-only Publish
// preview and this editor stay pixel-identical.
constexpr int kHitRadius = 6;
constexpr int kCurveHitRadius = 5;
constexpr int kPointRadius = 4; // anchor outer radius (DIP)
constexpr int kStrokeUnselected = 2;
constexpr int kStrokeSelected = 4;
constexpr int kStrokeAxis = 2; // axis line width (px, no DPI scaling - matches kGridColor pen and 2DBed convention)
constexpr int kAxisArrowHalf = 5; // half-base of the axis arrow triangle (DIP)
constexpr int kAxisArrowLen = 10; // length of the axis arrow triangle (DIP)
// Light-mode design tokens. Resolved through StateColor::darkModeColorFor()
// at paint time so the editor follows the app theme (#EEEEEE -> #4C4C55, #6B6B6B ->
@@ -46,12 +37,6 @@ const wxColour kAxisColor (107, 107, 107); // #6B6B6B grey 700
const wxColour kLabelMuted (107, 107, 107); // #6B6B6B grey 700
const wxColour kLabelStrong ( 38, 46, 48); // #262E30 grey 900
const wxColour kOutlineColor(172, 172, 172); // #ACACAC dimmed elements
// LAB (DeltaE76) threshold for "curve color is too close to the background": below it the curve
// gets a subtle outline so it does not visually vanish, otherwise it is drawn plain. Looser than
// the 5.0 of FlushPredict::is_similar_color, so a pastel pink on white still gets an outline.
constexpr float kBgSimilarThreshold = 15.0f;
constexpr int kOutlineExtraDip = 2;
} // namespace
GradientCurveEditor::GradientCurveEditor(wxWindow* parent,
@@ -177,15 +162,8 @@ void GradientCurveEditor::emit_changed()
wxRect GradientCurveEditor::plot_rect() const
{
const wxSize sz = GetClientSize();
const int x = static_cast<int>(std::lround(sz.x * kPlotLeftRatio));
const int y = static_cast<int>(std::lround(sz.y * kPlotTopRatio));
const int x2 = static_cast<int>(std::lround(sz.x * kPlotRightRatio));
const int y2 = static_cast<int>(std::lround(sz.y * kPlotBottomRatio));
// Force square 1:1 so X/Y axes share the same scale and grid cells stay square. Anchor at
// the top-left so the "100%" labels on the bottom/right still align with the plot edges.
const int side = std::max(1, std::min(x2 - x, y2 - y));
return wxRect(x, y, side, side);
// Square 1:1 plot, shared with the Publish dialog's read-only preview.
return mixed_gradient_plot_rect(GetClientSize());
}
wxPoint2DDouble GradientCurveEditor::data_to_px_f(double x, double y) const
@@ -330,171 +308,52 @@ void GradientCurveEditor::on_paint(wxPaintEvent& /*evt*/)
raw_dc.SetBackground(wxBrush(bg));
raw_dc.Clear();
// Render through wxGCDC so curves, arrows and anchor circles get anti-aliased; the buffered
// DC is the actual back buffer that gets blitted to the window.
wxGCDC dc(raw_dc);
// The curve and its anchors are drawn straight on the graphics context so their
// coordinates stay sub-pixel accurate (see data_to_px_f).
wxGraphicsContext* gc = dc.GetGraphicsContext();
const wxRect rc = plot_rect();
if (rc.width <= 0 || rc.height <= 0)
return;
// 10x10 light grid (10 lines including outer borders, 9 equal divisions).
dc.SetPen(wxPen(grid_color, 1));
for (int i = 0; i <= kGridDivisions; ++i) {
const int x = rc.x + rc.width * i / kGridDivisions;
const int y = rc.y + rc.height * i / kGridDivisions;
dc.DrawLine(x, rc.y, x, rc.y + rc.height);
dc.DrawLine(rc.x, y, rc.x + rc.width, y);
}
// Set the label font first so text width measurements drive arrow / label placement.
wxFont label_font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT);
label_font.SetPointSize(std::max(7, label_font.GetPointSize() - 1));
dc.SetFont(label_font);
const wxString axis_y_title = _L("Material Ratio");
const wxString axis_x_title = _L("Model Height");
const wxString pct_text = wxT("100%");
const wxSize x_title_sz = dc.GetTextExtent(axis_x_title);
const wxSize y_title_sz = dc.GetTextExtent(axis_y_title);
wxFont strong_font = label_font;
strong_font.SetWeight(wxFONTWEIGHT_SEMIBOLD);
dc.SetFont(strong_font);
const wxSize pct_text_sz = dc.GetTextExtent(pct_text);
dc.SetFont(label_font);
// Axes (grey 700) with filled triangle arrows. Y-axis extends above the plot top to the
// canvas top edge; X-axis extends past the plot right toward the canvas right edge.
const int arrow_half = FromDIP(kAxisArrowHalf);
const int arrow_len = FromDIP(kAxisArrowLen);
const wxSize sz = GetClientSize();
dc.SetPen(wxPen(axis_color, kStrokeAxis));
dc.SetBrush(wxBrush(axis_color));
// Y-axis: vertical line at plot_left, from arrow tip near canvas top down to plot bottom.
const int y_axis_x = rc.x;
const int y_title_pct_gap = FromDIP(1);
const int y_title_bottom_pad = FromDIP(2);
const int y_title_y = std::max(0, rc.y - y_title_sz.y - y_title_pct_gap - pct_text_sz.y - y_title_bottom_pad);
const int y_arrow_tip_y = y_title_y;
const int y_arrow_ty = y_arrow_tip_y + arrow_len;
dc.DrawLine(y_axis_x, y_arrow_ty, y_axis_x, rc.y + rc.height);
{
wxPoint tri[3] = {
wxPoint(y_axis_x, y_arrow_tip_y),
wxPoint(y_axis_x - arrow_half, y_arrow_ty),
wxPoint(y_axis_x + arrow_half, y_arrow_ty),
// Render the plot (grid, axes, labels, curves, anchors) through the shared painter so the
// interactive editor and the Publish dialog's read-only preview stay pixel-identical. The
// curves are handed over as sub-pixel polylines and anti-alias inside the helper.
std::vector<MixedGradientCurve> curves;
std::vector<wxPoint2DDouble> anchors;
if (m_points.size() >= 2) {
auto color_for_curve = [&](int curve_idx) -> wxColour {
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
if (c.Alpha() == 0)
c.Set(c.Red(), c.Green(), c.Blue(), 150);
return c;
};
dc.DrawPolygon(3, tri);
}
// X-axis arrow tip: stays just past the plot ideally, but is clamped so the trailing
// "Material Ratio" label still fits inside the canvas without overlapping the arrow.
const int x_axis_y = rc.y + rc.height;
const int x_label_gap = FromDIP(4);
const int x_edge_pad = FromDIP(6);
const int x_arrow_ideal = rc.x + rc.width + FromDIP(10);
const int x_arrow_max = sz.x - x_title_sz.x - x_label_gap - x_edge_pad - arrow_len;
const int x_arrow_tx = std::max(rc.x + rc.width + arrow_len,
std::min(x_arrow_ideal, x_arrow_max));
const int x_arrow_tip_x = x_arrow_tx + arrow_len;
const int x_title_x = x_arrow_tip_x + x_label_gap;
dc.DrawLine(rc.x, x_axis_y, x_arrow_tx, x_axis_y);
{
wxPoint tri[3] = {
wxPoint(x_arrow_tip_x, x_axis_y),
wxPoint(x_arrow_tx, x_axis_y - arrow_half),
wxPoint(x_arrow_tx, x_axis_y + arrow_half),
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
const int samples = std::max(128, plot_rect().width * 2);
std::vector<wxPoint2DDouble> poly;
poly.reserve(samples + 1);
for (int s = 0; s <= samples; ++s) {
const double x = double(s) / samples;
const double y0 = sample_curve_y(x);
const double vy = to_visual_y(curve_idx, y0);
poly.push_back(data_to_px_f(x, vy));
}
return poly;
};
dc.DrawPolygon(3, tri);
}
// Labels.
// "Model Height" and "100%" share the same left x; the gap is larger than the
// axis-arrow half-base so the text never visually touches the Y-axis arrow.
const int label_left_x = y_axis_x + FromDIP(10);
dc.SetTextForeground(label_muted);
dc.DrawText(axis_y_title, label_left_x, y_title_y);
dc.SetFont(strong_font);
dc.SetTextForeground(label_strong);
dc.DrawText(pct_text, label_left_x, y_title_y + y_title_sz.y + y_title_pct_gap);
// Bottom-right "100%" sits under the right end of the plot; "Material Ratio" follows the
// X-axis arrow tip (placement was already clamped above to leave room).
dc.DrawText(pct_text, rc.x + rc.width - pct_text_sz.x, x_axis_y);
dc.SetFont(label_font);
dc.SetTextForeground(label_muted);
dc.DrawText(axis_x_title, x_title_x, x_axis_y - x_title_sz.y / 2);
if (m_points.size() < 2 || !gc)
return;
auto color_for_curve = [&](int curve_idx) -> wxColour {
wxColour c = (curve_idx == 0) ? m_color_low : m_color_high;
// Transparent filaments (alpha == 0, e.g. #FFFFFF00) would be invisible.
// Lift alpha so the curve stays visible while still hinting at transparency.
if (c.Alpha() == 0)
c.Set(c.Red(), c.Green(), c.Blue(), 150);
return c;
};
auto build_polyline = [&](int curve_idx) -> std::vector<wxPoint2DDouble> {
const int samples = std::max(128, rc.width * 2);
std::vector<wxPoint2DDouble> poly;
poly.reserve(samples + 1);
for (int s = 0; s <= samples; ++s) {
const double x = double(s) / samples;
const double y0 = sample_curve_y(x);
const double vy = to_visual_y(curve_idx, y0);
poly.push_back(data_to_px_f(x, vy));
// Draw unselected first so the selected curve sits on top.
const int other = 1 - m_selected_curve;
for (const int idx : {other, m_selected_curve}) {
std::vector<wxPoint2DDouble> pts = build_polyline(idx);
if (pts.empty())
continue;
curves.push_back({std::move(pts), color_for_curve(idx), idx == m_selected_curve ? kStrokeSelected : kStrokeUnselected});
}
return poly;
};
// Only the geometry goes through the graphics context: dc.DrawLines() takes integer wxPoint
// and would quantize the curve back to whole pixels. The pen is still set on the dc, which
// forwards it here while keeping its own cached state in sync for later dc drawing.
auto draw_polyline = [&](const std::vector<wxPoint2DDouble>& poly, const wxColour& col, int stroke_dip) {
dc.SetPen(wxPen(col, FromDIP(stroke_dip)));
gc->StrokeLines(poly.size(), poly.data());
};
// Outline only when the curve color is perceptually close to the background; otherwise
// the plain filament color reads fine and the extra stroke would look heavy.
auto needs_outline = [&](const wxColour& c) {
return calc_color_distance(c, bg) < kBgSimilarThreshold;
};
auto draw_one = [&](int curve_idx, int stroke_dip) {
const auto poly = build_polyline(curve_idx);
const wxColour col = color_for_curve(curve_idx);
if (needs_outline(col))
draw_polyline(poly, outline_color, stroke_dip + kOutlineExtraDip);
draw_polyline(poly, col, stroke_dip);
};
// Draw unselected first so the selected curve sits on top.
const int other = 1 - m_selected_curve;
draw_one(other, kStrokeUnselected);
draw_one(m_selected_curve, kStrokeSelected);
// Control points (selected curve only): hollow circle with axis-color border, theme-aware fill.
// Drawn on the graphics context with a sub-pixel center so the ring stays centered on the
// curve instead of drifting up to half a pixel off it; pen and brush go through the dc for
// the same reason as in draw_polyline above.
const double r = FromDIP(kPointRadius);
dc.SetPen(wxPen(axis_color, 1));
dc.SetBrush(wxBrush(point_fill));
for (size_t i = 0; i < m_points.size(); ++i) {
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
const wxPoint2DDouble p = data_to_px_f(m_points[i].x, vy);
gc->DrawEllipse(p.m_x - r, p.m_y - r, r * 2, r * 2);
// Control points (selected curve only).
anchors.reserve(m_points.size());
for (size_t i = 0; i < m_points.size(); ++i) {
const double vy = to_visual_y(m_selected_curve, m_points[i].y);
anchors.push_back(data_to_px_f(m_points[i].x, vy));
}
}
const MixedGradientTheme theme{bg, grid_color, axis_color, label_muted, label_strong, outline_color, point_fill};
draw_mixed_gradient_plot(raw_dc, GetClientSize(), curves, anchors, theme);
}
void GradientCurveEditor::on_left_down(wxMouseEvent& evt)
+1
View File
@@ -174,6 +174,7 @@ void KBShortcutsDialog::fill_shortcuts()
{ ctrl + "O", L("Open Project") },
{ ctrl + "S", L("Save Project") },
{ ctrl + shift + "S", L("Save Project as")},
{ ctrl + shift + "E", L("Publish 3MF") },
// File>Import
{ ctrl + "I", L("Import geometry data from STL/STEP/3MF/OBJ/AMF files") },
// File>Export
+48 -6
View File
@@ -40,7 +40,6 @@
#include "Plater.hpp"
#include "WebViewDialog.hpp"
#include "../Utils/Process.hpp"
#include "format.hpp"
// BBS
#include "PartPlate.hpp"
#include "Preferences.hpp"
@@ -51,11 +50,9 @@
#include "../Utils/NetworkAgentFactory.hpp"
#include "../Utils/PrintHost.hpp"
#include <fstream>
#include <string_view>
#include "GUI_App.hpp"
#include "UnsavedChangesDialog.hpp"
#include "PublishSettingsDialog.hpp"
#include "MsgDialog.hpp"
#include "Notebook.hpp"
#include "GUI_Factories.hpp"
@@ -778,6 +775,10 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
if (m_plater) { m_plater->add_file(); }
return;
}
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'E') {
if (can_export_model()) publish_project();
return;
}
evt.Skip();
});
@@ -1774,6 +1775,22 @@ bool MainFrame::save_project_as(const wxString& filename)
return ret;
}
void MainFrame::publish_project()
{
if (m_plater == nullptr)
return;
// Seed the dialog from the session selection (a remembered state or a freshly loaded
// published 3MF); a null pointer means "fresh", keeping the dirty defaults.
std::vector<std::string> pending_keys;
std::vector<Slic3r::PublishedMaterialEntry> pending_material;
const bool has_prior = m_plater->get_pending_published(pending_keys, pending_material);
PublishSettingsDialog dlg(this, has_prior ? &pending_keys : nullptr, has_prior ? &pending_material : nullptr);
if (dlg.ShowModal() != wxID_OK)
return;
m_plater->set_pending_published(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
m_plater->export_published_3mf(dlg.GetPublishedKeys(), dlg.GetPublishedMaterialKeys());
}
bool MainFrame::can_upload() const
{
return true;
@@ -2869,6 +2886,20 @@ void MainFrame::init_menubar_as_editor()
[this](){return m_plater != nullptr && can_save_as(); }, this);
#endif
// BBS: publish
fileMenu->AppendSeparator();
auto publish_handler = [this](wxCommandEvent&) { publish_project(); };
#ifndef __APPLE__
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
publish_handler, "menu_publish", nullptr,
[this](){return can_export_model(); }, this);
#else
append_menu_item(fileMenu, wxID_ANY, _L("Publish 3MF") + dots + "\t" + ctrl + shift + "E", _L("Export a 3MF file with the selected settings embedded"),
publish_handler, "", nullptr,
[this](){return can_export_model(); }, this);
#endif
fileMenu->AppendSeparator();
@@ -4188,15 +4219,23 @@ std::wstring MainFrame::FileHistory::GetThumbnailUrl(int index) const
return wss.str();
}
bool MainFrame::FileHistory::GetPublished(int index) const
{
return index >= 0 && index < static_cast<int>(m_published_files.size()) && m_published_files[index];
}
void MainFrame::FileHistory::AddFileToHistory(const wxString &file)
{
if (this->m_fileMaxFiles == 0)
return;
wxFileHistory::AddFileToHistory(file);
if (m_load_called)
if (m_load_called) {
m_thumbnails.push_front(bbs_3mf_get_thumbnail(into_u8(file).c_str()));
else
m_published_files.push_front(bbs_3mf_is_published(into_u8(file)));
} else {
m_thumbnails.push_front("");
m_published_files.push_front(false);
}
}
void MainFrame::FileHistory::RemoveFileFromHistory(size_t i)
@@ -4205,6 +4244,7 @@ void MainFrame::FileHistory::RemoveFileFromHistory(size_t i)
return;
wxFileHistory::RemoveFileFromHistory(i);
m_thumbnails.erase(m_thumbnails.begin() + i);
m_published_files.erase(m_published_files.begin() + i);
}
size_t MainFrame::FileHistory::FindFileInHistory(const wxString & file)
@@ -4220,6 +4260,7 @@ void MainFrame::FileHistory::LoadThumbnails()
if (!thumbnail.empty()) {
m_thumbnails[i] = thumbnail;
}
m_published_files[i] = bbs_3mf_is_published(into_u8(GetHistoryFile(i)));
}
});
m_load_called = true;
@@ -4240,6 +4281,7 @@ void MainFrame::get_recent_projects(boost::property_tree::wptree &tree, int imag
std::wstring proj = m_recent_projects.GetHistoryFile(i).ToStdWstring();
item.put(L"project_name", proj.substr(proj.find_last_of(L"/\\") + 1));
item.put(L"path", proj);
item.put(L"published", m_recent_projects.GetPublished(i) ? L"1" : L"0");
boost::system::error_code ec;
std::time_t t = boost::filesystem::last_write_time(proj, ec);
if (!ec) {
+4
View File
@@ -192,6 +192,7 @@ class MainFrame : public DPIFrame
{
FileHistory(int max) : wxFileHistory(max) {}
std::wstring GetThumbnailUrl(int index) const;
bool GetPublished(int index) const;
virtual void AddFileToHistory(const wxString &file);
virtual void RemoveFileFromHistory(size_t i);
@@ -202,6 +203,7 @@ class MainFrame : public DPIFrame
void SetMaxFiles(int max);
private:
std::deque<std::string> m_thumbnails;
std::deque<bool> m_published_files; // parallel to m_thumbnails: is it a published 3mf?
bool m_load_called = false;
};
@@ -355,6 +357,8 @@ public:
bool can_upload() const;
void save_project();
bool save_project_as(const wxString& filename = wxString());
// Open the Publish dialog and export the selected settings as a published 3MF.
void publish_project();
void add_to_recent_projects(const wxString& filename);
void get_recent_projects(boost::property_tree::wptree &tree, int images);
+13 -129
View File
@@ -845,23 +845,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider()
m_ratio_bar->Bind(wxEVT_PAINT, [this](wxPaintEvent&) {
wxBufferedPaintDC dc(m_ratio_bar);
wxSize sz = m_ratio_bar->GetClientSize();
wxColour col_a = comp_colour(0), col_b = comp_colour(1);
for (int x = 0; x < sz.GetWidth(); ++x) {
double t = (double)x / sz.GetWidth();
wxColour c = blend_colors(col_a, col_b, 1.0 - t);
dc.SetPen(wxPen(c));
dc.DrawLine(x, 0, x, sz.GetHeight());
}
int div_x = (int)(ratio(1) / 100.0 * sz.GetWidth());
// Fixed in both themes, like the triangle picker's drag handle: the divider is drawn over
// blended filament colour, so it has to keep its contrast against data rather than chrome.
dc.SetPen(wxPen(wxColour(80, 80, 80), FromDIP(4)));
dc.DrawLine(div_x, 0, div_x, sz.GetHeight());
dc.SetPen(wxPen(*wxWHITE, FromDIP(2)));
dc.DrawLine(div_x, 0, div_x, sz.GetHeight());
draw_mixed_ratio_blend_bar(dc, wxRect(0, 0, sz.GetWidth(), sz.GetHeight()),
comp_colour(0), comp_colour(1), ratio(1) / 100.0);
});
m_ratio_bar->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
@@ -917,52 +902,8 @@ wxBoxSizer* MixedFilamentDialog::create_ratio_slider()
}
// ---- Triangle (ternary) ratio picker ----
// Barycentric coordinate utilities
struct TriPoint { double x, y; };
static double tri_signed_area2(TriPoint a, TriPoint b, TriPoint c)
{
return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
}
static bool tri_contains(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
{
double total = tri_signed_area2(v0, v1, v2);
if (std::abs(total) < 1e-9) return false;
double s0 = tri_signed_area2(p, v1, v2) / total;
double s1 = tri_signed_area2(v0, p, v2) / total;
double s2 = 1.0 - s0 - s1;
return s0 >= -0.001 && s1 >= -0.001 && s2 >= -0.001;
}
static void tri_barycentric(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2,
double& w0, double& w1, double& w2)
{
double total = std::abs(tri_signed_area2(v0, v1, v2));
if (total < 1e-9) { w0 = w1 = w2 = 1.0 / 3.0; return; }
w0 = std::abs(tri_signed_area2(p, v1, v2)) / total;
w1 = std::abs(tri_signed_area2(v0, p, v2)) / total;
w2 = 1.0 - w0 - w1;
w0 = std::clamp(w0, 0.0, 1.0);
w1 = std::clamp(w1, 0.0, 1.0);
w2 = std::clamp(w2, 0.0, 1.0);
double s = w0 + w1 + w2;
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
}
static TriPoint tri_clamp(TriPoint p, TriPoint v0, TriPoint v1, TriPoint v2)
{
double w0, w1, w2;
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
w0 = std::clamp(w0, 0.0, 1.0);
w1 = std::clamp(w1, 0.0, 1.0);
w2 = std::clamp(w2, 0.0, 1.0);
double s = w0 + w1 + w2;
if (s > 0) { w0 /= s; w1 /= s; w2 /= s; }
return {w0 * v0.x + w1 * v1.x + w2 * v2.x,
w0 * v0.y + w1 * v1.y + w2 * v2.y};
}
// The barycentric utilities (TriPoint, tri_contains, tri_barycentric, tri_clamp) live in
// FilamentBitmapUtils so the Publish dialog can mirror this picker read-only.
wxBoxSizer* MixedFilamentDialog::create_triangle_picker()
{
@@ -996,72 +937,15 @@ wxBoxSizer* MixedFilamentDialog::create_triangle_picker()
wxSize sz = m_triangle_panel->GetClientSize();
auto [v0, v1, v2] = get_vertices();
wxColour tri_bg = StateColor::darkModeColorFor(*wxWHITE);
dc.SetBrush(wxBrush(tri_bg));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
wxColour c0 = comp_colour(0), c1 = comp_colour(1), c2 = comp_colour(2);
const bool cache_valid = m_tri_cache_bmp.IsOk() &&
m_tri_cache_size == sz &&
m_tri_cache_c0 == c0 && m_tri_cache_c1 == c1 && m_tri_cache_c2 == c2;
if (!cache_valid) {
int min_y = (int)std::min({v0.y, v1.y, v2.y});
int max_y = (int)std::max({v0.y, v1.y, v2.y});
int min_x = (int)std::min({v0.x, v1.x, v2.x});
int max_x = (int)std::max({v0.x, v1.x, v2.x});
m_tri_cache_bmp = wxBitmap(sz.GetWidth(), sz.GetHeight(), 24);
wxMemoryDC mdc(m_tri_cache_bmp);
mdc.SetBrush(wxBrush(tri_bg));
mdc.SetPen(*wxTRANSPARENT_PEN);
mdc.DrawRectangle(0, 0, sz.GetWidth(), sz.GetHeight());
for (int py = min_y; py <= max_y; ++py) {
for (int px = min_x; px <= max_x; ++px) {
TriPoint p = {(double)px, (double)py};
if (!tri_contains(p, v0, v1, v2)) continue;
double w0, w1, w2;
tri_barycentric(p, v0, v1, v2, w0, w1, w2);
unsigned char mr, mg, mb;
if (w0 + w1 > 1e-6) {
float t01 = static_cast<float>(w1 / (w0 + w1));
Slic3r::filament_mixer_lerp(c0.Red(), c0.Green(), c0.Blue(),
c1.Red(), c1.Green(), c1.Blue(),
t01, &mr, &mg, &mb);
float t2 = static_cast<float>(w2);
Slic3r::filament_mixer_lerp(mr, mg, mb,
c2.Red(), c2.Green(), c2.Blue(),
t2, &mr, &mg, &mb);
} else {
mr = c2.Red(); mg = c2.Green(); mb = c2.Blue();
}
mdc.SetPen(wxPen(wxColour(mr, mg, mb)));
mdc.DrawPoint(px, py);
}
}
mdc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#CECECE")), 1));
mdc.SetBrush(*wxTRANSPARENT_BRUSH);
wxPoint pts[3] = {{(int)v0.x, (int)v0.y}, {(int)v1.x, (int)v1.y}, {(int)v2.x, (int)v2.y}};
mdc.DrawPolygon(3, pts);
mdc.SelectObject(wxNullBitmap);
m_tri_cache_c0 = c0; m_tri_cache_c1 = c1; m_tri_cache_c2 = c2;
m_tri_cache_size = sz;
}
dc.DrawBitmap(m_tri_cache_bmp, 0, 0);
// Drag handle (always redrawn on top of cached bitmap)
double hx = m_tri_wx * v0.x + m_tri_wy * v1.x + m_tri_wz * v2.x;
double hy = m_tri_wx * v0.y + m_tri_wy * v1.y + m_tri_wz * v2.y;
int handle_r = FromDIP(5);
dc.SetBrush(*wxWHITE_BRUSH);
dc.SetPen(wxPen(wxColour("#262E30"), FromDIP(2)));
dc.DrawCircle((int)hx, (int)hy, handle_r);
// Draw the background, cached barycentric fill, outline and drag-handle marker through the
// shared picker painter (same geometry the read-only Publish preview uses).
draw_mixed_triangle_picker(dc, sz,
{comp_colour(0), comp_colour(1), comp_colour(2)},
{m_tri_wx, m_tri_wy, m_tri_wz},
{StateColor::darkModeColorFor(*wxWHITE),
StateColor::darkModeColorFor(wxColour("#CECECE")),
StateColor::darkModeColorFor(wxColour("#262E30")),
StateColor::darkModeColorFor(COLOR_LABEL_MUTED)});
if (m_result.ratios.size() >= 3) {
dc.SetFont(::Label::Body_10);
-4
View File
@@ -169,10 +169,6 @@ private:
// Triangle picker drag point (barycentric weights)
double m_tri_wx{0.333}, m_tri_wy{0.333}, m_tri_wz{0.334};
// Cached triangle color bitmap (invalidated when colors or size change)
wxBitmap m_tri_cache_bmp;
wxColour m_tri_cache_c0, m_tri_cache_c1, m_tri_cache_c2;
wxSize m_tri_cache_size;
std::array<RatioLabelPanel*, 3> m_triangle_ratio_labels{nullptr, nullptr, nullptr};
};
+10 -36
View File
@@ -3083,7 +3083,7 @@ bool NotificationManager::push_notification_data(std::unique_ptr<NotificationMan
}
bool retval = false;
if (this->activate_existing(notification.get())) {
if (m_initialized) { // ignore update action - it cant be initialized if canvas and imgui context is not ready
if (m_initialized && m_imgui_ready) {
if (notification->get_type() == NotificationType::SlicingWarning) {
m_pop_notifications.back()->append(notification->get_data().ori_text);
} else {
@@ -3129,6 +3129,10 @@ void NotificationManager::stop_delayed_notifications_of_type(const NotificationT
void NotificationManager::render_notifications(GLCanvas3D &canvas, float overlay_width, float bottom_margin, float right_margin)
{
// Notifications render inside an ImGui frame, so the font atlas is built from this point on
// and pushed notifications may safely measure their text.
m_imgui_ready = true;
sort_notifications();
float bottom_up_last_y = bottom_margin; // ORCA dont scale margins
@@ -3339,17 +3343,7 @@ size_t NotificationManager::get_notification_count() const
void NotificationManager::bbl_show_plateinfo_notification(const std::string &text)
{
NotificationData data{NotificationType::BBLPlateInfo, NotificationLevel::PrintInfoNotificationLevel, BBL_NOTICE_MAX_INTERVAL, text};
for (std::unique_ptr<PopNotification> &notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::BBLPlateInfo) {
notification->reinit();
notification->update(data);
return;
}
}
auto notification = std::make_unique<NotificationManager::PopNotification>(data, m_id_provider, m_evt_handler);
push_notification_data(std::move(notification), 0);
push_notification_data(data, 0);
}
void NotificationManager::bbl_close_3mf_warn_notification()
@@ -3360,20 +3354,10 @@ void NotificationManager::bbl_close_3mf_warn_notification()
}
}
void NotificationManager::bbl_show_3mf_warn_notification(const std::string &text)
void NotificationManager::bbl_show_3mf_warn_notification(const std::string &text, NotificationLevel level)
{
NotificationData data{NotificationType::BBL3MFInfo, NotificationLevel::ErrorNotificationLevel, BBL_NOTICE_MAX_INTERVAL, text};
for (std::unique_ptr<PopNotification> &notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::BBL3MFInfo) {
notification->reinit();
notification->update(data);
return;
}
}
auto notification = std::make_unique<NotificationManager::PopNotification>(data, m_id_provider, m_evt_handler);
push_notification_data(std::move(notification), 0);
NotificationData data{NotificationType::BBL3MFInfo, level, BBL_NOTICE_MAX_INTERVAL, text};
push_notification_data(data, 0);
}
void NotificationManager::bbl_close_plateinfo_notification()
@@ -3388,17 +3372,7 @@ void NotificationManager::bbl_close_plateinfo_notification()
void NotificationManager::bbl_show_preview_only_notification(const std::string &text)
{
NotificationData data{NotificationType::BBLPreviewOnlyMode, NotificationLevel::WarningNotificationLevel, 0, text};
for (std::unique_ptr<PopNotification> &notification : m_pop_notifications) {
if (notification->get_type() == NotificationType::BBLPreviewOnlyMode) {
notification->reinit();
notification->update(data);
return;
}
}
auto notification = std::make_unique<NotificationManager::PopNotification>(data, m_id_provider, m_evt_handler);
push_notification_data(std::move(notification), 0);
push_notification_data(data, 0);
}
void NotificationManager::bbl_close_preview_only_notification()
+12 -2
View File
@@ -378,7 +378,9 @@ public:
void bbl_close_plateinfo_notification();
//BBS-- 3mf warning
void bbl_show_3mf_warn_notification(const std::string &text);
// level defaults to the historical error styling; callers reporting informational
// 3MF load notices (published settings) pass WarningNotificationLevel instead.
void bbl_show_3mf_warn_notification(const std::string &text, NotificationLevel level = NotificationLevel::ErrorNotificationLevel);
void bbl_close_3mf_warn_notification();
//BBS--preview only mode
@@ -1052,6 +1054,11 @@ private:
bool m_is_dark = false;
// set by init(), until false notifications are only added not updated and frame is not requested after push
bool m_initialized{ false };
// set by render_notifications() on the first rendered frame. m_initialized only proves the
// manager exists, not that the ImGui context can measure text: the font atlas is built lazily
// in ImGuiWrapper::new_frame() on the first GL render, so updating a notification before that
// (PopNotification::init -> count_spaces -> ImGui::CalcTextSize) dereferences a null font.
bool m_imgui_ready{ false };
// Target for wxWidgets events sent by clicking on the hyperlink available at some notifications.
wxEvtHandler* m_evt_handler;
// Cache of IDs to identify and reuse ImGUI windows.
@@ -1076,7 +1083,10 @@ private:
NotificationType::ProgressBar,
NotificationType::PrintHostUpload,
NotificationType::SimplifySuggestion,
NotificationType::ValidateWarning
NotificationType::ValidateWarning,
// A published file load can produce several distinct 3MF warnings (invalid values,
// skipped settings, changed slots); let them stack rather than clobber each other.
NotificationType::BBL3MFInfo
};
//prepared (basic) notifications
// non-static so its not loaded too early. If static, the translations wont load correctly.
+399 -38
View File
@@ -1,12 +1,10 @@
#include "Plater.hpp"
#include "../Utils/NetworkAgent.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r_version.h"
#include <cstddef>
#include <algorithm>
#include <chrono>
#include <numeric>
#include <limits>
#include <optional>
@@ -16,8 +14,6 @@
#include <vector>
#include <string>
#include <regex>
#include <future>
#include <thread>
#include <atomic>
#include <mutex>
#include <boost/algorithm/string.hpp>
@@ -69,7 +65,6 @@
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/SLA/Hollowing.hpp"
#include "libslic3r/SLA/SupportPoint.hpp"
#include "libslic3r/SLA/ReprojectPointsOnMesh.hpp"
#include "libslic3r/Polygon.hpp"
@@ -78,16 +73,15 @@
#include "libslic3r/SLAPrint.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PublishSettings.hpp"
#include "slic3r/Utils/CrealityPrint.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ObjColorUtils.hpp"
// For stl export
#include "libslic3r/CSGMesh/ModelToCSGMesh.hpp"
#include "libslic3r/CSGMesh/PerformCSGMeshBooleans.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
#include "GUI_ObjectList.hpp"
#ifdef __WXGTK__
#include "LinuxDisplayBackend.hpp"
@@ -123,7 +117,6 @@
#include "SendMultiMachinePage.hpp"
#include "SendToPrinter.hpp"
#include "PublishDialog.hpp"
#include "ModelMall.hpp"
#include "ConfigWizard.hpp"
#include "SyncAmsInfoDialog.hpp"
#include "../Utils/ASCIIFolding.hpp"
@@ -149,7 +142,6 @@
#include "ParamsDialog.hpp"
#include "ImageDPIFrame.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/RoundedRectangle.hpp"
#include "Widgets/RadioGroup.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/Button.hpp"
@@ -6712,6 +6704,12 @@ struct Plater::priv
SendToPrinterDialog* m_send_to_sdcard_dlg = nullptr;
PublishDialog *m_publish_dlg = nullptr;
// Session-level stash of the last published selection. Written on publish and on
// loading a published 3MF; read when the Publish dialog is opened.
bool m_has_pending_published{false};
std::vector<std::string> m_pending_published_keys;
std::vector<Slic3r::PublishedMaterialEntry> m_pending_material_keys;
// Data
Slic3r::DynamicPrintConfig *config; // FIXME: leak?
Slic3r::Print fff_print;
@@ -6936,7 +6934,10 @@ struct Plater::priv
// BBS: backup & restore
using LoadProgressCallback = std::function<bool(int, const wxString&)>;
std::vector<size_t> load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi = false);
std::vector<size_t> load_files(const std::vector<fs::path>& input_files,
LoadStrategy strategy,
bool ask_multi = false,
bool* published_out = nullptr);
std::vector<size_t> load_model_objects(const ModelObjectPtrs& model_objects, bool allow_negative_z = false, bool split_object = false, bool auto_drop = true);
// Texture-to-color import: a mesh loaded with UVs + a texture map gets its faces clustered
@@ -6964,7 +6965,7 @@ struct Plater::priv
std::function<bool()> cancel_callback = {});
fs::path get_export_file_path(GUI::FileType file_type);
wxString get_export_file(GUI::FileType file_type);
wxString get_export_file(GUI::FileType file_type, const wxString& title = {}, bool published = false);
// BBS
void load_auxiliary_files();
@@ -8268,7 +8269,10 @@ void read_binary_stl(const std::string& filename, std::string& model_id, std::st
}
// BBS: backup & restore
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi)
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
LoadStrategy strategy,
bool ask_multi,
bool* published_out)
{
std::vector<size_t> empty_result;
bool dlg_cont = true;
@@ -8348,6 +8352,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
const float INPUT_FILES_RATIO = 0.7;
const float INIT_MODEL_RATIO = 0.75;
const float CENTER_AROUND_ORIGIN_RATIO = 0.8;
const float LOAD_MODEL_RATIO = 0.9;
for (size_t i = 0; i < input_files.size(); ++i) {
@@ -8388,6 +8393,11 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
DynamicPrintConfig config;
Semver file_version;
En3mfType en_3mf_file_type = En3mfType::From_BBS;
// BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
// on load keep the user's current presets and overlay only those keys. Declared
// here (outside the config block below) so it stays alive for the embedded-preset
// gate, the metadata strip and the preset overlay after the block closes.
PublishedConfig published_config;
{
DynamicPrintConfig config_loaded;
@@ -8415,6 +8425,107 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
<< boost::format(", plate_data.size %1%, project_preset.size %2%, is_bbs_or_orca_3mf %3%, file_version %4% \n") % plate_data.size() %
project_presets.size() % (en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) % file_version.to_string();
// BBS: a "published" 3MF carries a flag plus the author-selected setting keys;
// on load keep the user's current presets and overlay only those keys. Parsed
// here (before the version/fallback chain below) because a published file has
// no project_settings.config: its values travel in the published_config
// metadata payload, which must fill config_loaded before the chain decides
// whether to import geometry only.
if (model.model_info != nullptr) {
auto published_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG);
if (published_it != model.model_info->metadata_items.end() && is_published_3mf_flag(published_it->second)) {
published_config.published = true;
auto keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG);
if (keys_it != model.model_info->metadata_items.end()) {
try {
auto j = nlohmann::json::parse(keys_it->second);
if (j.is_array())
for (const auto& k : j)
if (k.is_string())
published_config.published_keys.emplace_back(k.get<std::string>());
} catch (...) {
// Ignore malformed published_keys; the project still loads normally.
}
}
auto material_keys_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG);
if (material_keys_it != model.model_info->metadata_items.end()) {
try {
auto jm = nlohmann::json::parse(material_keys_it->second);
if (jm.is_array())
for (const auto& m : jm) {
try {
// Malformed entries are isolated so one bad item
// cannot discard valid entries that follow it.
if (!m.is_object())
continue;
PublishedMaterialEntry entry;
const auto mat_it = m.find("material");
if (mat_it != m.end() && mat_it->is_object()) {
const auto& mat = *mat_it;
if (mat.contains("filament_type") && mat["filament_type"].is_string())
entry.filament_type = mat["filament_type"].get<std::string>();
if (mat.contains("filament_vendor") && mat["filament_vendor"].is_string())
entry.filament_vendor = mat["filament_vendor"].get<std::string>();
if (mat.contains("filament_id") && mat["filament_id"].is_string())
entry.filament_id = mat["filament_id"].get<std::string>();
if (mat.contains("setting_id") && mat["setting_id"].is_string())
entry.setting_id = mat["setting_id"].get<std::string>();
if (mat.contains("name") && mat["name"].is_string())
entry.preset_name = mat["name"].get<std::string>();
}
if (m.contains("slot") && m["slot"].is_number_integer())
entry.slot = m["slot"].get<int>();
const auto entry_keys_it = m.find("keys");
if (entry_keys_it != m.end() && entry_keys_it->is_array())
for (const auto& k : *entry_keys_it)
if (k.is_string())
entry.keys.emplace_back(k.get<std::string>());
// Fields always written by the current exporter.
if (m.contains("full") && m["full"].is_boolean())
entry.full = m["full"].get<bool>();
const auto entry_full_keys_it = m.find("full_keys");
if (entry_full_keys_it != m.end() && entry_full_keys_it->is_array())
for (const auto& k : *entry_full_keys_it)
if (k.is_string())
entry.full_keys.emplace_back(k.get<std::string>());
if (m.contains("publish_type") && m["publish_type"].is_boolean())
entry.publish_type = m["publish_type"].get<bool>();
if (m.contains("type") && m["type"].is_string())
entry.publish_type_value = m["type"].get<std::string>();
if (m.contains("publish_color") && m["publish_color"].is_boolean())
entry.publish_color = m["publish_color"].get<bool>();
if (m.contains("color") && m["color"].is_string())
entry.color = m["color"].get<std::string>();
published_config.material_keys.emplace_back(std::move(entry));
} catch (const nlohmann::json::exception&) {
// Ignore only this malformed material entry.
}
}
} catch (const nlohmann::json::exception&) {
// Ignore malformed published_material_keys; the project still loads normally.
}
}
// Rebuild the published values from the metadata payload: a published
// file carries no project_settings.config, so config_loaded is filled
// from here; a missing or malformed payload leaves it empty and the
// fallback chain below imports the geometry only.
auto payload_it = model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG);
if (payload_it != model.model_info->metadata_items.end()) {
try {
ConfigSubstitutions payload_substitutions =
config_loaded.load_from_ini_string(payload_it->second, ForwardCompatibilitySubstitutionRule::Enable);
config_substitutions.substitutions.insert(config_substitutions.substitutions.end(),
std::make_move_iterator(payload_substitutions.begin()),
std::make_move_iterator(payload_substitutions.end()));
} catch (...) {
// Ignore malformed published_config; the project still loads normally.
}
}
}
}
// 1. add extruder for prusa model if the number of existing extruders is not enough
// 2. add extruder for BBS or Other model if only import geometry
if (en_3mf_file_type == En3mfType::From_Prusa || (load_model && !load_config)) {
@@ -8578,7 +8689,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
text += "\n";
log_and_show_3mf_info(text, bambu_project_title);
}
} else if (load_config) {
} else if (load_config && !published_config.published) {
// BambuStudio version is older or same as our SLIC3R_VERSION
wxString text = _L("The 3MF was created by BambuStudio. Some settings may differ from OrcaSlicer.");
log_and_show_3mf_info(text, bambu_project_title);
@@ -8638,7 +8749,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
Semver old_version(1, 5, 9);
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && load_model && load_config && !config_loaded.empty()) {
// A published 3MF has no project config to migrate: skip the old-version
// translations even if a slicer tag slipped through classification.
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && (file_version < old_version) && !published_config.published && load_model && load_config && !config_loaded.empty()) {
translate_old = true;
partplate_list.get_plate_size(current_width, current_depth, current_height);
}
@@ -8660,8 +8773,10 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
}
// BBS:: project embedded presets
if ((project_presets.size() > 0) && load_config) {
// BBS:: project embedded presets (skipped for published projects: the author's
// embedded presets must not pollute the receiver's library, the overlay applies
// the published keys to the receiver's own presets instead).
if ((project_presets.size() > 0) && load_config && !published_config.published) {
// load project embedded presets
PresetsConfigSubstitutions preset_substitutions;
PresetBundle & preset_bundle = *wxGetApp().preset_bundle;
@@ -8717,6 +8832,19 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
}
// BBS: a "published" 3MF loads as a new project: its path must not become the
// project filename (Save/Ctrl-S would overwrite the shared file), and the
// published metadata is consumed above and stripped so a later save is a normal
// unpublished 3MF.
if (published_out != nullptr && published_config.published)
*published_out = true;
if (published_config.published && load_config && this->model.model_info != nullptr) {
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_TAG);
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_KEYS_TAG);
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_MATERIAL_TAG);
this->model.model_info->metadata_items.erase(ORCA_PUBLISHED_CONFIG_TAG);
}
if (load_config) {
if (!config.empty()) {
Preset::normalize(config);
@@ -8725,7 +8853,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
{
// BBS: modify the prime tower params for old version file
Semver old_version3(2, 0, 0);
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && file_version < old_version3) {
if ((en_3mf_file_type == En3mfType::From_BBS || en_3mf_file_type == En3mfType::From_Orca) && !published_config.published && file_version < old_version3) {
double old_filament_prime_volume = 0.;
int filament_count = 0;
{
@@ -8775,7 +8903,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
}
auto choise = wxGetApp().app_config->get("no_warn_when_modified_gcodes");
if (choise.empty() || choise != "true") {
if (!published_config.published && (choise.empty() || choise != "true")) {
// BBS: first validate the printer
// validate the system profiles
std::set<std::string> modified_gcodes;
@@ -8820,12 +8948,56 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
if (wipe_tower_y_opt)
file_wipe_tower_y = *wipe_tower_y_opt;
// Convert the printer's filament ids to Orca ids before loading.
if (auto* agent = wxGetApp().getAgent()) {
if (auto* ids = config.opt<ConfigOptionStrings>("filament_ids"))
for (std::string& id : ids->values)
id = agent->to_orca_filament_id(id);
}
preset_bundle->load_config_model(filename.string(), std::move(config), file_version);
preset_bundle->load_config_model(filename.string(), std::move(config), file_version, &published_config);
// Mixed-filament definitions that collided with one of the
// receiver's real slots were relocated during the preset load.
// Re-point the freshly parsed model's extruder references and
// color painting from the author's slot numbers to where each
// definition landed, so volumes colored with a mix follow it.
// Runs before the objects are handed over to the plater below.
if (load_model && !published_config.mixed_slot_relocations.empty())
Slic3r::remap_model_filament_slots(model, published_config.mixed_slot_relocations);
// BBS: notify the user about published settings that could not be applied.
if (!published_config.skipped_keys.empty()) {
NotificationManager* notify_manager = q->get_notification_manager();
std::string message = _u8L("Some published settings could not be applied:");
for (const std::string& key : published_config.skipped_keys)
message += "\n-" + key;
// Informational: the load succeeded, these keys were skipped.
notify_manager
->bbl_show_3mf_warn_notification(message,
NotificationManager::NotificationLevel::WarningNotificationLevel);
}
// BBS: notify the user about slot materials that were replaced while
// loading a published project (type mismatch / no same-type match).
if (!published_config.material_replacements.empty()) {
NotificationManager* notify_manager = q->get_notification_manager();
std::string message = _u8L("Some filament slots were changed:");
for (const std::string& replacement : published_config.material_replacements)
message += "\n-" + replacement;
// Informational: the load succeeded, the slots were adapted.
notify_manager
->bbl_show_3mf_warn_notification(message,
NotificationManager::NotificationLevel::WarningNotificationLevel);
}
// Remember the imported published selection so the Publish dialog is
// pre-seeded with the file's settings. Stored after the load so any
// per-slot relocations are already reflected in material_keys.
if (published_config.published) {
this->m_has_pending_published = true;
this->m_pending_published_keys = published_config.published_keys;
this->m_pending_material_keys = published_config.material_keys;
}
ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type");
if (bed_type_opt != nullptr) {
@@ -8966,7 +9138,13 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
dynamic_map->value = false;
}
// Update filament combobox after loading config
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
if (published_config.published) {
q->update_filament_colors_in_full_config();
wxGetApp().plater()->sidebar().update_all_preset_comboboxes();
wxGetApp().plater()->sidebar().update_dynamic_filament_list();
} else {
wxGetApp().plater()->sidebar().update_presets(Preset::TYPE_FILAMENT);
}
// The loaded project supplies nozzle_volume_type; refresh the sidebar
// nozzle-count badges against it.
if (auto *nozzle_volumes = wxGetApp().preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type")) {
@@ -9719,7 +9897,7 @@ fs::path Plater::priv::get_export_file_path(GUI::FileType file_type)
return output_file;
}
wxString Plater::priv::get_export_file(GUI::FileType file_type)
wxString Plater::priv::get_export_file(GUI::FileType file_type, const wxString& title, bool published)
{
wxString wildcard;
switch (file_type) {
@@ -9761,8 +9939,11 @@ wxString Plater::priv::get_export_file(GUI::FileType file_type)
}
case FT_3MF:
{
output_file.replace_extension("3mf");
dlg_title = _L("Save file as");
// A published export is suggested as "<name>.published.3mf" so the role is visible in the
// dialog and in the recent-files list. This is only a pre-filled suggestion; the user's
// typed filename wins, keeping a plain ".3mf" output fully valid.
output_file.replace_extension(published ? "published.3mf" : "3mf");
dlg_title = title.empty() ? _L("Save file as") : title;
break;
}
case FT_OBJ:
@@ -10000,6 +10181,11 @@ void Plater::priv::reset(bool apply_presets_change)
clear_warnings();
// A new project must not inherit the previous project's published selection (Feature A/B).
m_has_pending_published = false;
m_pending_published_keys.clear();
m_pending_material_keys.clear();
set_project_filename("");
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: empty";
@@ -15091,14 +15277,15 @@ void Plater::load_project(wxString const& filename2,
if (strategy & LoadStrategy::Restore)
input_paths.push_back(into_u8(originfile));
std::vector<size_t> res = load_files(input_paths, strategy);
bool loaded_published = false;
std::vector<size_t> res = load_files(input_paths, strategy, false, &loaded_published);
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config);
// if res is empty no data has been loaded
if (!res.empty() && (load_restore || !(strategy & LoadStrategy::Silence))) {
if (!res.empty() && !loaded_published && (load_restore || !(strategy & LoadStrategy::Silence))) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " call set_project_filename: " << (load_restore ? originfile : filename);
p->set_project_filename(load_restore ? originfile : filename);
if (load_restore && originfile.IsEmpty()) {
@@ -15109,6 +15296,15 @@ void Plater::load_project(wxString const& filename2,
if (using_exported_file()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " using ecported set project filename: " << filename;
p->set_project_filename(filename);
} else if (loaded_published && !res.empty()) {
// A "published" 3MF loads as a new project: its path must not become the project
// filename (Save/Ctrl-S prompts for a destination instead of overwriting it);
// reset() already cleared the project name, so restore the default title and keep
// the file in recents. Only on a successful load (res not empty): a failed or
// cancelled load must not pollute "Recently opened".
p->set_project_name(_L("Untitled"));
if (!filename.IsEmpty())
wxGetApp().mainframe->add_to_recent_projects(filename);
}
}
@@ -16766,22 +16962,12 @@ void Plater::force_update_all_plate_thumbnails()
}
// BBS: backup
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi) {
std::vector<size_t> Plater::load_files(const std::vector<fs::path>& input_files, LoadStrategy strategy, bool ask_multi, bool* published_out) {
//BBS: wish to reset state when load a new file
p->m_slice_all_only_has_gcode = false;
//BBS: wish to reset all plates stats item selected state when load a new file
p->preview->get_canvas3d()->reset_select_plate_toolbar_selection();
return p->load_files(input_files, strategy, ask_multi);
}
// To be called when providing a list of files to the GUI slic3r on command line.
std::vector<size_t> Plater::load_files(const std::vector<std::string>& input_files, LoadStrategy strategy, bool ask_multi)
{
std::vector<fs::path> paths;
paths.reserve(input_files.size());
for (const std::string& path : input_files)
paths.emplace_back(path);
return p->load_files(paths, strategy, ask_multi);
return p->load_files(input_files, strategy, ask_multi, published_out);
}
bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path)
@@ -18010,7 +18196,6 @@ void Plater::send_gcode_finish(wxString name)
auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name);
p->notification_manager->push_exporting_finished_notification(out_str, "", false);
}
void Plater::export_core_3mf()
{
wxString path = p->get_export_file(FT_3MF);
@@ -18019,6 +18204,182 @@ void Plater::export_core_3mf()
export_3mf(path_u8, SaveStrategy::Silence);
}
// Export the current project as a "published" 3MF: a pure export that never touches the
// project's file name, dirty state, backup path or title, and attaches the published metadata
// to the model only for the duration of the export (a later Save Project is a normal 3MF).
int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
const std::vector<Slic3r::PublishedMaterialEntry>& material_keys)
{
wxString path = p->get_export_file(FT_3MF, _L("Publish 3MF file as:"), true);
if (path.empty() || path == "<cancel>")
return wxID_CANCEL;
nlohmann::json j = nlohmann::json::array();
for (const std::string& key : published_keys)
j.push_back(key);
nlohmann::json jm = nlohmann::json::array();
for (const Slic3r::PublishedMaterialEntry& e : material_keys)
jm.push_back({{"material",
{{"filament_type", e.filament_type},
{"filament_vendor", e.filament_vendor},
{"filament_id", e.filament_id},
{"setting_id", e.setting_id},
{"name", e.preset_name}}},
{"slot", e.slot},
{"keys", e.keys},
{"full", e.full},
{"full_keys", e.full_keys},
{"publish_type", e.publish_type},
{"type", e.publish_type_value},
{"publish_color", e.publish_color},
{"color", e.color}});
Model& model = this->model();
// Save the previous metadata so it can be restored after the export, keeping the in-memory
// project pristine (the published flag lives only in the exported file).
const bool had_model_info = (model.model_info != nullptr);
const bool had_published = had_model_info &&
(model.model_info->metadata_items.find(ORCA_PUBLISHED_TAG) != model.model_info->metadata_items.end());
const bool had_published_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_KEYS_TAG) !=
model.model_info->metadata_items.end());
const bool had_material_keys = had_model_info && (model.model_info->metadata_items.find(ORCA_PUBLISHED_MATERIAL_TAG) !=
model.model_info->metadata_items.end());
const bool had_payload = had_model_info &&
(model.model_info->metadata_items.find(ORCA_PUBLISHED_CONFIG_TAG) != model.model_info->metadata_items.end());
const std::string prev_published = had_published ? model.model_info->metadata_items.at(ORCA_PUBLISHED_TAG) : std::string();
const std::string prev_published_keys = had_published_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_KEYS_TAG) :
std::string();
const std::string prev_material_keys = had_material_keys ? model.model_info->metadata_items.at(ORCA_PUBLISHED_MATERIAL_TAG) :
std::string();
const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string();
// export_3mf() assigns archive paths to previously unsaved SVGs. Preserve those fields too,
// otherwise a publish changes what a later normal project save writes.
std::vector<std::pair<std::string*, std::string>> previous_svg_paths;
for (ModelObject* object : model.objects)
for (ModelVolume* volume : object->volumes)
if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) {
std::string* path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf;
previous_svg_paths.emplace_back(path_in_3mf, *path_in_3mf);
}
auto restore_temporary_state = [&]() {
for (const auto& [path_in_3mf, previous_path] : previous_svg_paths)
*path_in_3mf = previous_path;
if (!had_model_info) {
model.model_info = nullptr;
} else {
if (had_published)
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = prev_published;
else
model.model_info->metadata_items.erase(ORCA_PUBLISHED_TAG);
if (had_published_keys)
model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = prev_published_keys;
else
model.model_info->metadata_items.erase(ORCA_PUBLISHED_KEYS_TAG);
if (had_material_keys)
model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = prev_material_keys;
else
model.model_info->metadata_items.erase(ORCA_PUBLISHED_MATERIAL_TAG);
if (had_payload)
model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = prev_payload;
else
model.model_info->metadata_items.erase(ORCA_PUBLISHED_CONFIG_TAG);
}
};
bool state_restored = false;
auto restore_now = [&]() {
if (state_restored)
return;
restore_temporary_state();
state_restored = true;
};
ScopeGuard restore_guard(restore_now);
if (model.model_info == nullptr)
model.model_info = std::make_shared<ModelInfo>();
model.model_info->metadata_items[ORCA_PUBLISHED_TAG] = "1";
model.model_info->metadata_items[ORCA_PUBLISHED_KEYS_TAG] = j.dump();
model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] = jm.dump();
int ret = -1;
try {
// Minimal published export: filter full_config to the published keys, material keys,
// identity fields and plate geometry keys, and omit the project config file, the
// project-embedded preset dumps and the OrcaSlicer version tag from the archive. The
// filtered values are serialized into the published_config metadata payload instead, so
// OrcaSlicer versions without the publish feature fall back to importing the geometry only
// (keeping the receiver's presets) while new versions rebuild the config from the payload.
DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config_secure();
DynamicPrintConfig filtered_cfg = filter_published_config(full_cfg, published_keys, material_keys);
std::string payload;
for (const std::string& key : filtered_cfg.keys()) {
// A value containing a newline would break the INI written below (read_ini throws),
// so load_from_ini_string discards the whole settings block on import. Skip such
// keys instead of silently dropping every setting.
std::string value = filtered_cfg.opt_serialize(key);
if (value.find('\n') != std::string::npos) {
BOOST_LOG_TRIVIAL(warning) << "publish: dropping key \"" << key
<< "\" from the published payload (value contains a newline)";
continue;
}
payload += key + " = " + value + "\n";
}
model.model_info->metadata_items[ORCA_PUBLISHED_CONFIG_TAG] = std::move(payload);
// Same file layout as save_project(), plus Silence (so export_3mf does not set the project
// filename on success, keeping this a pure export like export_core_3mf()) and MinimalPublished.
auto save_strategy = SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence | SaveStrategy::MinimalPublished;
bool full_pathnames = wxGetApp().app_config->get_bool("export_sources_full_pathnames");
if (full_pathnames)
save_strategy = save_strategy | SaveStrategy::FullPathSources;
ret = export_3mf(into_path(path), save_strategy, -1, nullptr);
} catch (...) {
restore_now();
MessageDialog(this,
_L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs "
"have the file open."),
_L("Publish"), wxOK | wxICON_WARNING)
.ShowModal();
return wxID_CANCEL;
}
if (ret < 0) {
restore_now();
MessageDialog(this,
_L("Failed to export the published 3MF file.\nPlease check whether the folder exists online or if other programs "
"have the file open."),
_L("Publish"), wxOK | wxICON_WARNING)
.ShowModal();
return wxID_CANCEL;
}
restore_now();
// Register the exported file in the "Recently opened" list
wxGetApp().mainframe->add_to_recent_projects(path);
return wxID_YES;
}
bool Plater::get_pending_published(std::vector<std::string>& out_keys,
std::vector<Slic3r::PublishedMaterialEntry>& out_material) const
{
if (!p->m_has_pending_published)
return false;
out_keys = p->m_pending_published_keys;
out_material = p->m_pending_material_keys;
return true;
}
void Plater::set_pending_published(const std::vector<std::string>& published_keys,
const std::vector<Slic3r::PublishedMaterialEntry>& material_keys)
{
p->m_has_pending_published = true;
p->m_pending_published_keys = published_keys;
p->m_pending_material_keys = material_keys;
}
Preset *get_printer_preset(const MachineObject *obj)
{
if (!obj)
+8 -3
View File
@@ -407,9 +407,7 @@ public:
bool preview_zip_archive(const boost::filesystem::path& archive_path);
// BBS: restore
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
// To be called when providing a list of files to the GUI slic3r on command line.
std::vector<size_t> load_files(const std::vector<std::string>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false);
std::vector<size_t> load_files(const std::vector<boost::filesystem::path>& input_files, LoadStrategy strategy = LoadStrategy::LoadModel | LoadStrategy::LoadConfig, bool ask_multi = false, bool* published_out = nullptr);
// to be called on drag and drop
bool load_files(const wxArrayString& filenames);
@@ -519,6 +517,13 @@ public:
void export_gcode_3mf(bool export_all = false);
void send_gcode_finish(wxString name);
void export_core_3mf();
// Export a "published" 3MF embedding the author-selected settings in the file metadata; a
// pure export that leaves the in-memory project untouched.
int export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
// Session-level stash of the last published selection, seeded into the Publish dialog on
// open and written on publish or on loading a published 3MF
bool get_pending_published(std::vector<std::string>& out_keys, std::vector<Slic3r::PublishedMaterialEntry>& out_material) const;
void set_pending_published(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
static TriangleMesh combine_mesh_fff(const ModelObject& mo, int instance_id, std::function<void(const std::string&)> notify_func = {});
void export_stl(bool extended = false, bool selection_only = false, bool multi_stls = false, FileType file_type = FT_STL);
//BBS: remove amf
File diff suppressed because it is too large Load Diff
+286
View File
@@ -0,0 +1,286 @@
#pragma once
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
#include "Widgets/TabCtrl.hpp"
#include "libslic3r/PublishSettings.hpp"
#include <wx/wx.h>
#include <wx/colour.h>
#include <wx/scrolwin.h>
#include <wx/menu.h>
#include <utility>
#include <vector>
#include <string>
// Forward declarations (all are global classes, see Widgets/TextInput.hpp and
// Widgets/StaticLine.hpp).
class TextInput;
class StaticLine;
namespace Slic3r { namespace GUI {
struct PublishMaterialIdentity
{
std::string type;
std::string vendor;
std::string id;
};
// One unmet dependency of an enabled mixed-filament slot: the component filament the mix uses
// would ship without its material (either the component slot is not enabled at all, or it is
// enabled with neither "Full Publish" nor the "Type" requirement checked). Slots are 0-based.
struct MixedDependencyIssue
{
enum class Reason { Disabled, MaterialNotPublished };
size_t mixed_slot{0};
size_t component_slot{0};
Reason reason{Reason::Disabled};
};
// Dialog letting a model author select which settings get embedded in a 3MF. Nested tab layout
// mirroring the Process settings (Printer / Filament / Process outer tabs, category or material
// tabs inside each). Dirty settings are pre-checked and shown bold; on OK the print rows become
// "orca_published_keys" and the material rows become "orca_published_material_keys".
class PublishSettingsDialog : public DPIDialog
{
public:
// Optional published selection (Feature A/B): when the caller supplies one (either a
// remembered session selection or the payload of a freshly loaded published 3MF) the dialog
// is seeded from it, overriding the dirty-default pre-check. A non-null pointer to an empty
// selection means "publish nothing" (an intentional empty state); a null pointer means "no
// remembered selection" (keep the dirty defaults).
PublishSettingsDialog(wxWindow* parent = nullptr,
const std::vector<std::string>* published_keys = nullptr,
const std::vector<Slic3r::PublishedMaterialEntry>* material_keys = nullptr);
~PublishSettingsDialog();
// The selected print/printer setting keys (in display order); printer keys carry a '#N'
// per-extruder suffix.
std::vector<std::string> GetPublishedKeys() const;
// The selected keys grouped per material section (base keys, no '#N' suffix).
std::vector<Slic3r::PublishedMaterialEntry> GetPublishedMaterialKeys() const;
protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
void on_sys_color_changed() override;
private:
void fit_to_content();
void refresh_mixed_tab_bitmaps();
// Which part of the settings the row/category came from.
enum class Section { Print, Printer, Material };
// One selectable setting row: a checkbox (setting name) plus a value label and an optional
// grey unit label. key is the full config key, possibly with a "#N" variant suffix
// (print/printer rows); material rows carry the base key.
enum class RowKind {
Setting, // a regular setting key
Color, // material colour requirement (filament_colour)
Type, // material type requirement (read-only text)
};
struct Row
{
std::string key;
wxString category;
wxString subcategory;
wxString label;
wxString value;
wxString unit;
wxString section_title; // outer tab title, for filter matching
Section section{Section::Print};
RowKind kind{RowKind::Setting};
size_t outer_index{0};
size_t inner_index{0};
bool dirty{false}; // matches a dirty base key: pre-checked + bold
bool matches_filter{false}; // survives the active filter (computed by apply_filter)
wxCheckBox* check{nullptr};
wxStaticText* value_label{nullptr};
wxStaticText* unit_label{nullptr};
wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value
wxSizerItem* item{nullptr}; // sizer item of this row's h-sizer in its tab list sizer
};
// An optgroup heading. Rows store indices into m_rows.
struct Subcategory
{
wxString title;
::StaticLine* header{nullptr}; // null when the title is empty
wxSizerItem* item{nullptr};
std::vector<size_t> rows;
};
// An inner TabCtrl page with its material controls and content.
struct Category
{
wxString title;
Section section{Section::Print};
size_t group{0}; // index into m_sections / outer page
size_t source_index{0}; // stable source page or material slot index
wxPanel* page{nullptr};
wxScrolledWindow* scroll{nullptr};
wxBoxSizer* list_sizer{nullptr};
wxStaticText* info{nullptr};
wxPoint scroll_pos{0, 0};
wxStaticBitmap* filament_color_chip{nullptr};
wxStaticText* title_label{nullptr}; // material title (static text; Full Publish carries the label elsewhere)
// "Enable": while unchecked nothing of this slot is exported and everything below the
// header row is hidden. For physical slots the Full Publish toggle sits on a second
// line (full_line_item) visible only when enabled; for mixed slots Enable alone implies
// publishing the mix definition, so no Full Publish widget exists at all.
wxCheckBox* enable_check{nullptr};
wxSizerItem* full_line_item{nullptr}; // sizer item of the Full Publish line (physical slots only)
// "Full Publish": while checked, the whole slot preset is serialized and its rows
// (incl. Color/Type) are disabled.
wxCheckBox* full_check{nullptr};
// True for a mixed-color filament slot: no Material/Retraction rows; Enable publishes
// the slot's gradient/ratio definition as a whole.
bool is_mixed{false};
// Material identity, only for Section::Material categories.
std::string filament_type;
std::string filament_vendor;
std::string filament_id;
// The author's 0-based filament slot this material section represents.
size_t filament_slot{0};
std::vector<Subcategory> subs;
std::vector<size_t> rows; // flattened rows of this category
};
// Frozen snapshot of a mixed filament slot's definition for the read-only visualization
// painted on the slot's page. Plain data only: the paint handler must never touch the
// config. For gradient slots the curve is pre-sampled (t, ratio) pairs, where ratio is the
// first component's share over model height; anchors carry the raw control points.
struct MixedVisualSpec
{
bool valid{false};
bool is_gradient{false};
std::vector<wxColour> component_colours; // colour per component, in config order
std::vector<double> ratios; // sublayer shares summing to ~1 (non-gradient)
std::vector<double> tri_weights; // 3-component mixes: barycentric shares
std::vector<std::pair<double, double>> gradient_samples;
std::vector<std::pair<double, double>> gradient_anchors;
};
// One outer TabCtrl page. Category entries are its inner tabs.
struct SectionGroup
{
wxString title; // _L("Printer") / _L("Filament") / _L("Process")
Section kind{Section::Print}; // maps 1:1 to the display group
std::string icon_name; // "printer" / "filament" / "process"
ScalableBitmap icon_bmp; // tab icon next to the title; rescaled on DPI change
wxPanel* page{nullptr};
TabCtrl* tabs{nullptr};
// Second tab strip, below the main one, listing only the mixed-color filament slots.
// Present on the Material section only (null elsewhere).
TabCtrl* mixed_tabs{nullptr};
wxPanel* page_host{nullptr};
wxBoxSizer* page_host_sizer{nullptr};
int selected_inner{-1};
// Selected mixed tab (index into mixed_categories), valid while a mixed slot page is shown.
int selected_mixed{-1};
std::vector<size_t> categories; // indices into m_categories (physical slots)
std::vector<size_t> mixed_categories; // indices into m_categories (mixed slots)
};
void build_option_model();
// Seed the dialog from a published selection (print/printer keys + per-slot material keys):
// the supplied selection is authoritative - it is applied after the dirty pre-check and
// overrides it, so deselected dirty keys stay off. Rows/slots not present in the selection
// are left unselected. Unknown or out-of-range entries are skipped gracefully.
void apply_selection(const std::vector<std::string>& published_keys,
const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
// Frozen snapshot of a mixed slot's definition for the page visualization, resolved from
// the full config once at dialog-build time. Gradient slots pre-sample exactly what the
// slicer will print: the custom curve wins over the gradient_range endpoints over the
// 0.10 -> 0.90 default (the resolution FilamentBitmapUtils::mixed_gradient_curve mirrors).
static MixedVisualSpec make_mixed_visual_spec(const Slic3r::DynamicPrintConfig& full, size_t slot);
void apply_filter(const wxString& filter_text);
// Menu-only pseudo filters: show only the checked ("Filter selected") or only the
// unchecked ("Filter non-selected") rows. The search box keeps the user's text.
void apply_pseudo_filter(bool selected_only);
// Recompute row matches and visibility for the active filter mode. filter is the lowered
// search text; it is ignored by the pseudo modes.
void refresh_filter(const wxString& filter);
void select_all(bool value);
void select_visible(bool value);
void show_menu(wxMouseEvent& evt);
void set_row_bold(Row& row, bool bold);
// "Full Publish" toggled: disables/enables the material's rows.
void on_full_toggle(size_t category_index);
// "Enable" toggled on a material slot: reveals/hides everything below the header and, for a
// mixed slot, auto-selects its component filaments' "Enable" + "Full Publish" toggles.
void on_enable_toggle(size_t category_index);
// Whether a category currently publishes something, driving its tab's indicator dot.
// Print/Printer: any row checked. Material: the slot's "Enable" is on.
bool category_has_selection(const Category& cat) const;
// Recompute the indicator dot on every outer/inner tab from the current selection state.
void refresh_tab_indicators();
// Unmet dependencies of enabled mixed-filament slots, one record per (mix, component) pair:
// "Enable" not checked on the component, or enabled with neither "Full Publish" nor the
// "Type" requirement row checked. Colour is deliberately ignored (the receiver renders the
// mix from its own components' colours). Sorted by mixed slot, then component slot.
std::vector<MixedDependencyIssue> unpublished_mixed_components() const;
// Read-only visualization of a mixed slot's definition (a stacked ratio bar, or the
// Material Ratio vs Model Height graph for a gradient), inserted above the info hint
// inside the category's scroll area.
void add_mixed_visual(size_t category_index, const MixedVisualSpec& spec);
// Return/create the fixed outer page for a Section kind.
size_t section_group_for(Section kind);
size_t category_index_for(const wxString& title,
Section section,
size_t group,
size_t source_index,
const PublishMaterialIdentity& identity = PublishMaterialIdentity(),
bool is_mixed = false);
size_t subcategory_index_for(size_t category_index, const wxString& title, const wxString& icon);
void add_row_ui(const std::string& key,
const wxString& label,
const wxString& value,
const wxString& unit,
size_t category_index,
size_t subcategory_index,
RowKind kind = RowKind::Setting);
// The non-structural filament keys of a slot's preset, for a "Full Publish" entry.
std::vector<std::string> full_keys_for_slot() const;
void save_scroll_position(Category& category);
void show_outer_page(size_t section_index);
void show_inner_page(size_t section_index, int inner_index);
void show_mixed_page(size_t section_index, int mixed_index);
void on_outer_tab_changed(wxCommandEvent& event);
void on_inner_tab_changed(size_t section_index, wxCommandEvent& event);
void on_mixed_tab_changed(size_t section_index, wxCommandEvent& event);
bool row_is_visible(const Row& row) const;
void apply_visibility();
void bind_tab_events();
TabCtrl* m_outer_tabs{nullptr};
wxPanel* m_outer_host{nullptr};
wxBoxSizer* m_outer_host_sizer{nullptr};
int m_selected_outer{-1};
wxBoxSizer* m_fb_sizer{nullptr}; // "All"/"None" buttons sizer
// Active filter mode: free text from the search box, or one of the menu's pseudo filters.
enum class FilterMode { Text, SelectedOnly, UnselectedOnly };
FilterMode m_filter_mode{FilterMode::Text};
TextInput* m_filter_box{nullptr};
wxTextCtrl* m_filter_ctrl{nullptr};
wxStaticBitmap* m_menu_button{nullptr};
// Shown while a pseudo filter is active (the search box keeps the user's text, so the chip
// carries the visible state); clicking it returns to text filtering.
wxStaticText* m_pseudo_chip{nullptr};
wxString m_info_nonsel;
wxString m_info_allsel;
wxString m_info_empty;
wxString m_info_mix; // body hint shown for a mixed slot (published as a whole)
ScalableBitmap m_search;
ScalableBitmap m_menu;
std::vector<Row> m_rows;
std::vector<Category> m_categories;
std::vector<SectionGroup> m_sections;
};
}} // namespace Slic3r::GUI
+8 -17
View File
@@ -7,6 +7,7 @@
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PublishSettings.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "Search.hpp"
@@ -5699,26 +5700,16 @@ if (is_marlin_flavor)
optgroup->append_single_option_line("extruder_offset", "printer_extruder_basic_information#extruder-offset-position", extruder_idx);
//BBS: don't show retract related config menu in machine page
// These optgroups are built from publishable_printer_retraction/z_hop_options() so the
// published-3MF printer allowlist (their union in libslic3r/PublishSettings.hpp) can
// never drift from what the machine page actually shows.
optgroup = page->new_optgroup(L("Retraction"), L"param_retraction");
optgroup->append_single_option_line("retraction_length", "printer_extruder_retraction#length", extruder_idx);
optgroup->append_single_option_line("retract_restart_extra", "printer_extruder_retraction#extra-length-on-restart", extruder_idx);
optgroup->append_single_option_line("retraction_speed", "printer_extruder_retraction#retraction-speed", extruder_idx);
optgroup->append_single_option_line("deretraction_speed", "printer_extruder_retraction#deretraction-speed", extruder_idx);
optgroup->append_single_option_line("retraction_minimum_travel", "printer_extruder_retraction#travel-distance-threshold", extruder_idx);
optgroup->append_single_option_line("retract_when_changing_layer", "printer_extruder_retraction#retract-on-layer-change", extruder_idx);
optgroup->append_single_option_line("wipe", "printer_extruder_retraction#wipe-while-retracting", extruder_idx);
optgroup->append_single_option_line("wipe_distance", "printer_extruder_retraction#wipe-distance", extruder_idx);
optgroup->append_single_option_line("retract_before_wipe", "printer_extruder_retraction#retract-amount-before-wipe", extruder_idx);
// Orca
optgroup->append_single_option_line("retract_after_wipe", "printer_extruder_retraction#retract-amount-after-wipe", extruder_idx);
for (const PublishablePrinterOption& opt : publishable_printer_retraction_options())
optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx);
optgroup = page->new_optgroup(L("Z-Hop"), L"param_extruder_lift_enforcement");
optgroup->append_single_option_line("retract_lift_enforce", "printer_extruder_z_hop#on-surfaces", extruder_idx);
optgroup->append_single_option_line("z_hop_types", "printer_extruder_z_hop#z-hop-type", extruder_idx);
optgroup->append_single_option_line("z_hop", "printer_extruder_z_hop#z-hop-height", extruder_idx);
optgroup->append_single_option_line("travel_slope", "printer_extruder_z_hop#traveling-angle", extruder_idx);
optgroup->append_single_option_line("retract_lift_above", "printer_extruder_z_hop#only-lift-z-above", extruder_idx);
optgroup->append_single_option_line("retract_lift_below", "printer_extruder_z_hop#only-lift-z-below", extruder_idx);
for (const PublishablePrinterOption& opt : publishable_printer_z_hop_options())
optgroup->append_single_option_line(opt.key, opt.icon, extruder_idx);
optgroup = page->new_optgroup(L("Retraction when switching material"), L"param_retraction_material_change");
optgroup->append_single_option_line("retract_length_toolchange", "printer_extruder_retraction#retraction-when-switching-materials", extruder_idx);
+1 -2
View File
@@ -30,7 +30,6 @@
#include <memory>
//#include "BedShapeDialog.hpp"
#include "Event.hpp"
#include "wxExtensions.hpp"
#include "ConfigManipulation.hpp"
#include "OptionsGroup.hpp"
@@ -38,7 +37,6 @@
//BBS: GUI refactor
#include "Notebook.hpp"
#include "ParamsPanel.hpp"
#include "Widgets/RoundedRectangle.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/CheckBox.hpp" // ORCA
@@ -472,6 +470,7 @@ protected:
std::string m_last_sparse_infill_rotate_template_value;
ConfigManipulation get_config_manipulation();
friend class EditGCodeDialog;
friend class PublishSettingsDialog;
};
class TabPrint : public Tab
+1 -223
View File
@@ -12,6 +12,7 @@
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Color.hpp"
#include "format.hpp"
#include "ConfigValueFormatter.hpp"
#include "GUI_App.hpp"
#include "Plater.hpp"
#include "Tab.hpp"
@@ -22,7 +23,6 @@
#include "MsgDialog.hpp"
#include "PresetComboBoxes.hpp"
#include "Widgets/RoundedRectangle.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/HyperLink.hpp"
@@ -571,14 +571,6 @@ void DiffModel::Clear()
}
static std::string get_pure_opt_key(std::string opt_key)
{
const int pos = opt_key.find("#");
if (pos > 0)
boost::erase_tail(opt_key, opt_key.size() - pos);
return opt_key;
}
// ----------------------------------------------------------------------------
// DiffViewCtrl
// ----------------------------------------------------------------------------
@@ -1212,32 +1204,6 @@ bool UnsavedChangesDialog::save(PresetCollection* dependent_presets, bool show_s
return true;
}
wxString get_string_from_enum(const std::string& opt_key, const DynamicPrintConfig& config, bool is_infill = false, int idx = -1)
{
const ConfigOptionDef& def = config.def()->options.at(opt_key);
const std::vector<std::string>& names = def.enum_labels;//ConfigOptionEnum<T>::get_enum_names();
int val = 0;
if (idx >= 0)
val = dynamic_cast<const ConfigOptionInts*>(config.option(opt_key))->get_at(idx);
else
val = config.option(opt_key)->getInt();
// Each infill doesn't use all list of infill declared in PrintConfig.hpp.
// So we should "convert" val to the correct one
if (is_infill) {
for (auto key_val : *def.enum_keys_map)
if (int(key_val.second) == val) {
auto it = std::find(def.enum_values.begin(), def.enum_values.end(), key_val.first);
if (it == def.enum_values.end())
return "";
return from_u8(_utf8(names[it - def.enum_values.begin()]));
}
return _L("Undefined");
}
return from_u8(_utf8(names[val]));
}
// BBS
#if 0
static size_t get_id_from_opt_key(std::string opt_key)
@@ -1251,194 +1217,6 @@ static size_t get_id_from_opt_key(std::string opt_key)
}
#endif
static wxString get_full_label(std::string opt_key, const DynamicPrintConfig& config)
{
opt_key = get_pure_opt_key(opt_key);
auto option = config.option(opt_key);
if (!option || option->is_nil())
return _L("N/A");
const ConfigOptionDef* opt = config.def()->get(opt_key);
return opt->full_label.empty() ? opt->label : opt->full_label;
}
static wxString get_string_value(std::string opt_key, const DynamicPrintConfig& config)
{
int orig_opt_idx = -1;
int opt_idx = -1;
int pos = opt_key.find("#");
std::string temp_str = opt_key;
if (pos > 0) {
boost::erase_head(temp_str, pos + 1);
orig_opt_idx = static_cast<size_t>(atoi(temp_str.c_str()));
}
opt_idx = orig_opt_idx >= 0 ? orig_opt_idx : 0;
opt_key = get_pure_opt_key(opt_key);
auto option = config.option(opt_key);
if (!option) {
return _L("N/A");
}
auto opt_vector = dynamic_cast<const ConfigOptionVectorBase *>(option);
if ((option->is_scalar() && config.option(opt_key)->is_nil()) ||
(option->is_vector() && opt_vector && opt_idx >= 0 && opt_idx < opt_vector->size() && opt_vector->is_nil(opt_idx)))
return _L("N/A");
wxString out;
const ConfigOptionDef* opt = config.def()->get(opt_key);
bool is_nullable = opt->nullable;
switch (opt->type) {
case coInt:
return from_u8((boost::format("%1%") % config.opt_int(opt_key)).str());
case coInts: {
if (is_nullable) {
auto values = config.opt<ConfigOptionIntsNullable>(opt_key);
if (opt_idx < values->size())
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
}
else {
auto values = config.opt<ConfigOptionInts>(opt_key);
if (orig_opt_idx >= 0 && orig_opt_idx < values->size()) {
return from_u8((boost::format("%1%") % values->get_at(opt_idx)).str());
}
else {
std::string value_str;
for (int i = 0; i < values->size(); i++) {
value_str += std::to_string(values->get_at(i));
if (i != values->size() - 1) {
value_str += ",";
}
}
return from_u8(value_str);
}
}
return _L("Undefined");
}
case coBool:
return config.opt_bool(opt_key) ? "true" : "false";
case coBools: {
if (is_nullable) {
auto values = config.opt<ConfigOptionBoolsNullable>(opt_key);
if (opt_idx < values->size())
return values->get_at(opt_idx) ? "true" : "false";
}
else {
auto values = config.opt<ConfigOptionBools>(opt_key);
if (opt_idx < values->size())
return values->get_at(opt_idx) ? "true" : "false";
}
return _L("Undefined");
}
case coPercent:
return from_u8((boost::format("%1%%%") % int(config.optptr(opt_key)->getFloat())).str());
case coPercents: {
if (is_nullable) {
auto values = config.opt<ConfigOptionPercentsNullable>(opt_key);
if (opt_idx < values->size())
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
}
else {
auto values = config.opt<ConfigOptionPercents>(opt_key);
if (opt_idx < values->size())
return from_u8((boost::format("%1%%%") % values->get_at(opt_idx)).str());
}
return _L("Undefined");
}
case coFloat:
return double_to_string(config.opt_float(opt_key));
case coFloats: {
if (is_nullable) {
auto values = config.opt<ConfigOptionFloatsNullable>(opt_key);
if (opt_idx < values->size())
return double_to_string(values->get_at(opt_idx));
}
else {
auto values = config.opt<ConfigOptionFloats>(opt_key);
if (values && opt_idx < values->size())
return double_to_string(values->get_at(opt_idx));
}
return _L("Undefined");
}
case coString:
return from_u8(config.opt_string(opt_key));
case coStrings: {
const ConfigOptionStrings* strings = config.opt<ConfigOptionStrings>(opt_key);
if (strings) {
if (opt_key == "compatible_printers" || opt_key == "compatible_prints") {
if (strings->empty())
return _L("All");
for (size_t id = 0; id < strings->size(); id++)
out += from_u8(strings->get_at(id)) + "\n";
out.RemoveLast(1);
return out;
}
if (!strings->empty() && opt_idx < strings->values.size())
return from_u8(strings->get_at(opt_idx));
}
break;
}
case coFloatOrPercent: {
const ConfigOptionFloatOrPercent* opt = config.opt<ConfigOptionFloatOrPercent>(opt_key);
if (opt)
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
return out;
}
case coEnum: {
return get_string_from_enum(opt_key, config,
opt_key == "top_surface_pattern" ||
opt_key == "bottom_surface_pattern" ||
opt_key == "internal_solid_infill_pattern" ||
opt_key == "sparse_infill_pattern" ||
opt_key == "ironing_pattern" ||
opt_key == "support_ironing_pattern" ||
opt_key == "support_pattern" ||
opt_key == "support_interface_pattern")
;
}
case coEnums: {
return get_string_from_enum(opt_key, config,
opt_key == "top_surface_pattern" ||
opt_key == "bottom_surface_pattern" ||
opt_key == "internal_solid_infill_pattern" ||
opt_key == "sparse_infill_pattern" ||
opt_key == "ironing_pattern" ||
opt_key == "support_ironing_pattern" ||
opt_key == "support_pattern" ||
opt_key == "support_interface_pattern"
, opt_idx);
}
case coPoint: {
Vec2d val = config.opt<ConfigOptionPoint>(opt_key)->value;
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
}
case coPoints: {
//BBS: add bed_exclude_area
if (opt_key == "printable_area" || opt_key == "thumbnails") {
ConfigOptionPoints points = *config.option<ConfigOptionPoints>(opt_key);
//BuildVolume build_volume = {points.values, 0.};
return get_thumbnails_string(points.values);
}
else if (opt_key == "bed_exclude_area") {
return get_thumbnails_string(config.option<ConfigOptionPoints>(opt_key)->values);
}
else if (opt_key == "head_wrap_detect_zone") {
return get_thumbnails_string(config.option<ConfigOptionPoints>(opt_key)->values);
}
else if (opt_key == "wrapping_exclude_area") {
return get_thumbnails_string(config.option<ConfigOptionPoints>(opt_key)->values);
}
Vec2d val = config.opt<ConfigOptionPoints>(opt_key)->get_at(opt_idx);
return from_u8((boost::format("[%1%]") % ConfigOptionPoint(val).serialize()).str());
}
default:
break;
}
return out;
}
void UnsavedChangesDialog::update(Preset::Type type, PresetCollection* dependent_presets, const std::string& new_selected_preset, const wxString& header)
{
PresetCollection* presets = dependent_presets;
+81 -6
View File
@@ -23,6 +23,7 @@
#include <wx/textdlg.h>
#include <wx/wx.h>
#include <wx/weakref.h>
#include <wx/display.h>
#include <wx/fileconf.h>
#include <wx/file.h>
@@ -562,6 +563,78 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
m_ProfileJson["filament"][fName]["selected"] = 1;
}
}
else if (strCmd == "check_for_new_printers") {
json response = json::object();
response["command"] = "check_new_printers_result";
// Guide pages currently send sequence_id as a number, while older
// pages may send it as a string. Preserve the value without
// forcing either representation.
if (j.contains("sequence_id"))
response["sequence_id"] = j["sequence_id"];
else
response["sequence_id"] = "";
if (!m_MainPtr->preset_updater) {
response["error"] = "Printer update service is unavailable.";
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
} else {
// Orca: enumerate vendors directly from disk rather than from m_ProfileJson["model"]
// — a vendor with no machine models (e.g. a filament-only bundle, or a test fixture
// like "test123" with an empty machine_model_list) never gets a "vendor" entry
// pushed into "model" by LoadProfileFamily(), so it would be invisible to the
// request body and get endlessly re-offered by the server. Scan both the system dir
// (already-installed vendors) and the bundled resources dir (shipped-but-not-yet-
// installed vendors), same as LoadProfileData() does when building loaded_vendors.
std::set<std::string> system_vendors;
for (const auto& dir : {vendor_dir, rsrc_vendor_dir}) {
if (!boost::filesystem::exists(dir))
continue;
for (const auto& entry : boost::filesystem::directory_iterator(dir)) {
if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json"))
system_vendors.insert(entry.path().stem().string());
}
}
// Orca: check_new_vendors() is async (network + confirmation dialog + download
// all happen off the calling thread apart from the dialog itself); guard against
// this dialog being closed before the callback fires.
wxWeakRef<GuideFrame> weak_this(this);
try {
m_MainPtr->preset_updater->check_new_vendors(
system_vendors, [weak_this, response](std::vector<std::string> installed_vendors, bool declined) mutable {
if (!weak_this)
return;
// Orca: append the newly installed vendor(s) into the in-memory
// profile data (instead of a full LoadProfileData() rescan of every
// vendor) and push the refreshed list to the webview, the same way
// request_userguide_profile does, so the printer list picks them up
// without needing to reopen the guide.
for (const auto& vendor_id : installed_vendors) {
weak_this->LoadProfileFamily(vendor_id, (weak_this->vendor_dir / (vendor_id + ".json")).string());
}
if (!installed_vendors.empty()) {
json profile_response = json::object();
profile_response["command"] = "response_userguide_profile";
profile_response["sequence_id"] = "10001";
profile_response["response"] = weak_this->m_ProfileJson;
wxString profileJS = wxString::Format("HandleStudio(%s)", profile_response.dump(-1, ' ', true));
weak_this->RunScript(profileJS);
}
response["vendors"] = installed_vendors;
response["declined"] = declined;
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
weak_this->RunScript(strJS);
});
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(warning) << "Failed to check for new printers: " << e.what();
response["error"] = "Failed to check for new printers.";
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
}
}
}
else if (strCmd == "user_guide_finish") {
SaveProfile();
@@ -1644,13 +1717,15 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath
OneModel["materials"] = pm["default_materials"];
// wxString strCoverPath = wxString::Format("%s\\%s\\%s_cover.png", strFolder, strVendor, std::string(s1.mb_str()));
std::string cover_file = s1 + "_cover.png";
boost::filesystem::path cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file).make_preferred();
std::string cover_file = s1 + "_cover.png";
boost::filesystem::path cover_path = boost::filesystem::absolute(vendor_dir / cover_file).make_preferred();
BOOST_LOG_TRIVIAL(info) << "[WebGuideDialog] " << cover_path;
if (!boost::filesystem::exists(cover_path)) {
cover_path =
(boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") /
cover_file)
.make_preferred();
cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file)
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
.make_preferred();
}
OneModel["cover"] = cover_path.string();
+35
View File
@@ -160,6 +160,15 @@ bool Button::GetValue() const { return state_handler.states() & StateHandler::Ch
void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; }
void Button::SetIndicator(bool on)
{
if (m_show_indicator == on)
return;
m_show_indicator = on;
messureSize();
Refresh();
}
void Button::SetVertical(bool vertical)
{
this->vertical = vertical;
@@ -313,6 +322,13 @@ void Button::render(wxDC& dc)
szContent.x -= d;
}
}
if (m_show_indicator) {
const int dot = FromDIP(6);
if (vertical)
szContent.y += dot + FromDIP(6);
else
szContent.x += dot + FromDIP(6);
}
// move to center
wxRect rcContent = {{0, 0}, size};
if (isCenter) {
@@ -354,6 +370,17 @@ void Button::render(wxDC& dc)
#endif
dc.DrawText(text, pt);
}
if (m_show_indicator) {
const int dot = FromDIP(6); // diameter
wxPoint dot_pt;
dot_pt.x = pt.x + (text.IsEmpty() ? 0 : textSize.x) + FromDIP(6) + dot / 2;
// Centre on the content vertically; a bitmap-only (empty-label) tab has no text row.
dot_pt.y = text.IsEmpty() ? rcContent.y + rcContent.height / 2 : pt.y + textSize.y / 2;
const wxColour c = StateColor::darkModeColorFor(m_indicator_color);
dc.SetBrush(wxBrush(c));
dc.SetPen(wxPen(c));
dc.DrawCircle(dot_pt, dot / 2);
}
}
void Button::messureSize()
@@ -380,6 +407,14 @@ void Button::messureSize()
szContent.y = szIcon.y;
}
}
if (m_show_indicator) {
// Indicator dot sits to the right of the label: its diameter plus the gap from the text.
const int dot = FromDIP(6);
if (vertical)
szContent.y += dot + FromDIP(6);
else
szContent.x += dot + FromDIP(6);
}
wxSize size = szContent + paddingSize * 2;
if (minSize.GetHeight() > 0)
size.SetHeight(minSize.GetHeight());
+7
View File
@@ -4,6 +4,7 @@
#include "../wxExtensions.hpp"
#include "StaticBox.hpp"
#include <wx/tipwin.h>
#include <wx/colour.h>
class ButtonProps
{
@@ -45,6 +46,8 @@ class Button : public StaticBox
bool canFocus = true;
bool isCenter = true;
bool vertical = false;
bool m_show_indicator = false;
wxColour m_indicator_color = wxColour("#009688");
static const int buttonWidth = 200;
static const int buttonHeight = 50;
@@ -78,6 +81,10 @@ public:
void SetSelected(bool selected = true) { m_selected = selected; }
// Show a small coloured dot to the right of the label (used by TabCtrl tabs to flag that
// the tab's category has a selected/toggled setting).
void SetIndicator(bool on);
// Only meant to be used by inspector, not public API
ButtonStyle GetStyle() const { return m_style; }
ButtonType GetType() const { return m_type; }
+7
View File
@@ -7,6 +7,7 @@ BEGIN_EVENT_TABLE(StaticBox, wxWindow)
// catch paint events
//EVT_ERASE_BACKGROUND(StaticBox::eraseEvent)
EVT_SIZE(StaticBox::sizeEvent)
EVT_PAINT(StaticBox::paintEvent)
END_EVENT_TABLE()
@@ -140,6 +141,12 @@ void StaticBox::eraseEvent(wxEraseEvent& evt)
#endif
}
void StaticBox::sizeEvent(wxSizeEvent& evt)
{
Refresh();
evt.Skip();
}
void StaticBox::paintEvent(wxPaintEvent& evt)
{
// depending on your system you may need to look at double-buffered dcs
+2
View File
@@ -46,6 +46,8 @@ public:
protected:
void eraseEvent(wxEraseEvent& evt);
void sizeEvent(wxSizeEvent& evt);
void paintEvent(wxPaintEvent& evt);
void render(wxDC& dc);
+96 -67
View File
@@ -2,8 +2,8 @@
#include <wx/dc.h>
wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent );
wxDEFINE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent );
wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent);
wxDEFINE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent);
BEGIN_EVENT_TABLE(TabCtrl, StaticBox)
@@ -22,11 +22,7 @@ END_EVENT_TABLE()
#define TAB_BUTTON_PADDING_Y 2
#define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y
TabCtrl::TabCtrl(wxWindow * parent,
wxWindowID id,
const wxPoint & pos,
const wxSize & size,
long style)
TabCtrl::TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos, const wxSize& size, long style)
: StaticBox(parent, id, pos, size, style)
{
#if 0
@@ -42,14 +38,11 @@ TabCtrl::TabCtrl(wxWindow * parent,
hsizer->Add(sizer, 0, wxEXPAND | wxBOTTOM, border_width * 4);
SetSizer(hsizer);
Bind(wxEVT_COMMAND_BUTTON_CLICKED, &TabCtrl::buttonClicked, this);
//wxString reason;
//IsTransparentBackgroundSupported(&reason);
// wxString reason;
// IsTransparentBackgroundSupported(&reason);
}
TabCtrl::~TabCtrl()
{
delete images;
}
TabCtrl::~TabCtrl() { delete images; }
int TabCtrl::GetSelection() const { return sel; }
@@ -75,15 +68,13 @@ void TabCtrl::SelectItem(int item)
Refresh();
}
void TabCtrl::Unselect()
{
SelectItem(-1);
}
void TabCtrl::Unselect() { SelectItem(-1); }
void TabCtrl::Rescale()
{
for (auto & b : btns)
for (auto& b : btns)
b->Rescale();
relayout();
}
bool TabCtrl::SetFont(wxFont const& font)
@@ -95,28 +86,32 @@ bool TabCtrl::SetFont(wxFont const& font)
return true;
}
int TabCtrl::AppendItem(const wxString &item,
int image, int selImage,
void * clientData)
int TabCtrl::AppendItem(const wxString& item, int image, int selImage, void* clientData)
{
Button * btn = new Button();
Button* btn = new Button();
btn->Create(this, item, "", wxBORDER_NONE);
btn->SetFont(GetFont());
btn->SetTextColor(StateColor(
std::make_pair(0x6B6B6C, (int) StateColor::NotChecked),
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Normal)));
btn->SetTextColor(
StateColor(std::make_pair(0x6B6B6C, (int) StateColor::NotChecked), std::make_pair(wxColour("#262E30"), (int) StateColor::Normal)));
btn->SetBackgroundColor(StateColor());
btn->SetCornerRadius(0);
btn->SetPaddingSize({TAB_BUTTON_PADDING});
btns.push_back(btn);
if (btns.size() > 1)
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, TAB_BUTTON_SPACE);
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, item_space);
sizer->AddStretchSpacer(1);
relayout();
return btns.size() - 1;
}
int TabCtrl::AppendItem(const wxString& item, const wxBitmap& bitmap, void* clientData)
{
const int index = AppendItem(item, -1, -1, clientData);
SetItemBitmap(index, bitmap);
return index;
}
bool TabCtrl::DeleteItem(int item)
{
if (item < 0 || item >= btns.size()) {
@@ -136,7 +131,7 @@ bool TabCtrl::DeleteItem(int item)
sizer->GetItem(sizer->GetItemCount() - 1)->SetMinSize({0, 0});
if (selection_changed) {
sel--; // `relayout()` uses `sel` so we need to update this before calling `relayout()`
sel--; // `relayout()` uses `sel` so we need to update this before calling `relayout()`
}
relayout();
if (selection_changed) {
@@ -159,75 +154,87 @@ void TabCtrl::DeleteAllItems()
unsigned int TabCtrl::GetCount() const { return btns.size(); }
wxString TabCtrl::GetItemText(unsigned int item) const
wxString TabCtrl::GetItemText(unsigned int item) const { return item < btns.size() ? btns[item]->GetLabel() : wxString{}; }
void TabCtrl::SetItemText(unsigned int item, wxString const& value)
{
return item < btns.size() ? btns[item]->GetLabel() : wxString{};
if (item >= btns.size())
return;
btns[item]->SetLabel(value);
}
void TabCtrl::SetItemText(unsigned int item, wxString const &value)
void TabCtrl::SetItemBitmap(unsigned int item, const wxBitmap& bitmap)
{
if (item >= btns.size()) return;
btns[item]->SetLabel(value);
if (item >= btns.size())
return;
btns[item]->SetIcon(bitmap);
relayout();
}
void TabCtrl::SetItemIndicator(unsigned int item, bool on)
{
if (item >= btns.size())
return;
btns[item]->SetIndicator(on);
relayout();
}
bool TabCtrl::GetItemBold(unsigned int item) const
{
if (item >= btns.size()) return false;
if (item >= btns.size())
return false;
return btns[item]->GetFont() == bold;
}
void TabCtrl::SetItemBold(unsigned int item, bool bold)
{
if (item >= btns.size()) return;
if (item >= btns.size())
return;
btns[item]->SetFont(bold ? this->bold : GetFont());
btns[item]->Rescale();
}
void* TabCtrl::GetItemData(unsigned int item) const
{
if (item >= btns.size()) return nullptr;
if (item >= btns.size())
return nullptr;
return btns[item]->GetClientData();
}
void TabCtrl::SetItemData(unsigned int item, void* clientData)
{
if (item >= btns.size()) return;
if (item >= btns.size())
return;
btns[item]->SetClientData(clientData);
}
void TabCtrl::AssignImageList(wxImageList* imageList)
{
if (images == imageList) return;
if (images == imageList)
return;
delete images;
images = imageList;
}
void TabCtrl::SetItemTextColour(unsigned int item, const StateColor &col)
void TabCtrl::SetItemTextColour(unsigned int item, const StateColor& col)
{
if (item >= btns.size()) return;
if (item >= btns.size())
return;
btns[item]->SetTextColor(col);
}
int TabCtrl::GetFirstVisibleItem() const
{
return btns.size() == 0 ? -1 : 0;
}
int TabCtrl::GetFirstVisibleItem() const { return btns.size() == 0 ? -1 : 0; }
int TabCtrl::GetNextVisible(int item) const
{
return ++item < btns.size() ? item : -1;
}
int TabCtrl::GetNextVisible(int item) const { return ++item < btns.size() ? item : -1; }
bool TabCtrl::IsVisible(unsigned int item) const
{
return true;
}
bool TabCtrl::IsVisible(unsigned int item) const { return true; }
void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
{
auto size = GetSize();
wxWindow::DoSetSize(x, y, width, height, sizeFlags);
if (size == GetSize()) return;
if (size == GetSize())
return;
relayout();
}
@@ -235,7 +242,9 @@ void TabCtrl::DoSetSize(int x, int y, int width, int height, int sizeFlags)
WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
{
if (nMsg == WM_GETDLGCODE) { return DLGC_WANTARROWS; }
if (nMsg == WM_GETDLGCODE) {
return DLGC_WANTARROWS;
}
return wxWindow::MSWWindowProc(nMsg, wParam, lParam);
}
@@ -244,15 +253,15 @@ WXLRESULT TabCtrl::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
void TabCtrl::relayout()
{
int offset = 10;
int item = sel + 1;
int first = 0;
int item = sel + 1;
int first = 0;
for (int i = 0; i < item; ++i)
offset += btns[i]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
offset += btns[i]->GetMinSize().x + item_space * 2;
if (item < btns.size())
offset += btns[item]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
int width = GetSize().x;
offset += btns[item]->GetMinSize().x + item_space * 2;
int width = GetSize().x;
for (int i = 0; i < btns.size(); ++i) {
auto size = btns[i]->GetMinSize().x + TAB_BUTTON_SPACE * 2;
auto size = btns[i]->GetMinSize().x + item_space * 2;
if (i < sel && offset > width) {
sizer->Show(i * 2 + 1, false);
sizer->Show(i * 2 + 2, false);
@@ -273,14 +282,32 @@ void TabCtrl::relayout()
sizer->GetItem(i * 2 + 2)->SetMinSize({0, 0});
}
if (item >= btns.size())
-- item;
--item;
// Keep spacing 2 ~ 10 TAB_BUTTON_SPACE
int b = GetSize().x - offset - 10 - (item + 1 - first) * TAB_BUTTON_SPACE * 8;
int b = GetSize().x - offset - 10 - (item + 1 - first) * item_space * 8;
sizer->GetItem(item * 2 + 2)->SetMinSize({b > 0 ? b : 0, 0});
Layout();
}
void TabCtrl::buttonClicked(wxCommandEvent &event)
void TabCtrl::SetItemSpace(int space)
{
if (space < 0 || space == item_space)
return;
item_space = space;
relayout();
Refresh();
}
int TabCtrl::GetFullSize() const
{
// Mirrors relayout(): a 10px leading spacer plus every button's min width and spacing.
int width = 10;
for (const Button* btn : btns)
width += btn->GetMinSize().x + item_space * 2;
return width;
}
void TabCtrl::buttonClicked(wxCommandEvent& event)
{
SetFocus();
auto btn = event.GetEventObject();
@@ -288,7 +315,7 @@ void TabCtrl::buttonClicked(wxCommandEvent &event)
SelectItem(iter == btns.end() ? -1 : iter - btns.begin());
}
void TabCtrl::keyDown(wxKeyEvent &event)
void TabCtrl::keyDown(wxKeyEvent& event)
{
switch (event.GetKeyCode()) {
case WXK_UP:
@@ -307,11 +334,13 @@ void TabCtrl::keyDown(wxKeyEvent &event)
void TabCtrl::doRender(wxDC& dc)
{
wxSize size = GetSize();
int states = state_handler.states();
if (sel < 0) { return; }
int states = state_handler.states();
if (sel < 0) {
return;
}
auto x1 = btns[sel]->GetPosition().x;
auto x2 = x1 + btns[sel]->GetSize().x;
auto x1 = btns[sel]->GetPosition().x;
auto x2 = x1 + btns[sel]->GetSize().x;
const int BS2 = (1 + border_width) / 2;
#if 0
const int BS = border_width / 2;
+29 -21
View File
@@ -3,32 +3,30 @@
#include "Button.hpp"
wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGING, wxCommandEvent );
wxDECLARE_EVENT( wxEVT_TAB_SEL_CHANGED, wxCommandEvent );
wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGING, wxCommandEvent);
wxDECLARE_EVENT(wxEVT_TAB_SEL_CHANGED, wxCommandEvent);
class TabCtrl : public StaticBox
{
std::vector<Button*> btns;
wxImageList* images = nullptr;
wxBoxSizer * sizer = nullptr;
wxBoxSizer* sizer = nullptr;
int sel = -1;
wxFont bold;
int item_space = 2; // space around each button, both sides (SetItemSpace)
public:
TabCtrl(wxWindow * parent,
wxWindowID id,
const wxPoint & pos = wxDefaultPosition,
const wxSize & size = wxDefaultSize,
long style = 0);
TabCtrl(wxWindow* parent, wxWindowID id, const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxDefaultSize, long style = 0);
~TabCtrl();
public:
virtual bool SetFont(wxFont const & font) override;
virtual bool SetFont(wxFont const& font) override;
public:
int AppendItem(const wxString &item, int image = -1, int selImage = -1, void *clientData = nullptr);
int AppendItem(const wxString& item, int image = -1, int selImage = -1, void* clientData = nullptr);
int AppendItem(const wxString& item, const wxBitmap& bitmap, void* clientData = nullptr);
bool DeleteItem(int item);
@@ -36,7 +34,7 @@ public:
unsigned int GetCount() const;
int GetSelection() const;
int GetSelection() const;
void SelectItem(int item);
@@ -45,15 +43,19 @@ public:
virtual void Rescale();
wxString GetItemText(unsigned int item) const;
void SetItemText(unsigned int item, wxString const &value);
void SetItemText(unsigned int item, wxString const& value);
void SetItemBitmap(unsigned int item, const wxBitmap& bitmap);
bool GetItemBold(unsigned int item) const;
void SetItemBold(unsigned int item, bool bold);
// Show/hide the small "has selection" dot next to a tab's text.
void SetItemIndicator(unsigned int item, bool on);
void* GetItemData(unsigned int item) const;
void SetItemData(unsigned int item, void *clientData);
void AssignImageList(wxImageList *imageList);
bool GetItemBold(unsigned int item) const;
void SetItemBold(unsigned int item, bool bold);
void* GetItemData(unsigned int item) const;
void SetItemData(unsigned int item, void* clientData);
void AssignImageList(wxImageList* imageList);
void SetItemTextColour(unsigned int item, const StateColor& col);
@@ -62,6 +64,12 @@ public:
int GetNextVisible(int item) const;
bool IsVisible(unsigned int item) const;
// Extra space around each tab button (in px on both sides). Defaults to the control-wide
// standard; call before appending items so every button picks it up.
void SetItemSpace(int space);
int GetFullSize() const;
private:
virtual void DoSetSize(int x, int y, int width, int height, int sizeFlags = wxSIZE_AUTO) override;
@@ -71,10 +79,10 @@ private:
void relayout();
void buttonClicked(wxCommandEvent & event);
void keyDown(wxKeyEvent &event);
void buttonClicked(wxCommandEvent& event);
void keyDown(wxKeyEvent& event);
void doRender(wxDC & dc) override;
void doRender(wxDC& dc) override;
// some useful events
bool sendTabCtrlEvent(bool changing = false);
+461 -66
View File
@@ -10,6 +10,7 @@
#include <set>
#include <string>
#include <thread>
#include <mutex>
#include <unordered_map>
#include <ostream>
#include <utility>
@@ -29,6 +30,7 @@
#include "libslic3r/format.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PresetCacheFormat.hpp"
#include "libslic3r_version.h"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
@@ -96,6 +98,8 @@ struct Update
bool forced_update;
//BBS: add directory support
bool is_directory {false};
// Orca: a vendor update may be the cache-only form.
bool is_opc {false};
Update() {}
//BBS: add directory support
@@ -126,13 +130,24 @@ struct Update
//BBS: add directory support
void install() const
{
if (is_directory) {
if (is_directory) {
copy_directory_recursively(source, target, file_filter);
}
else {
} else {
copy_file_fix(source, target);
// A vendor must be installed in exactly one form. Remove the
// representation that would otherwise be stale or shadow this one.
boost::system::error_code ec;
if (is_opc) {
fs::remove(target.parent_path() / (vendor + ".json"), ec);
ec.clear();
fs::remove_all(target.parent_path() / vendor, ec);
}
else {
fs::remove(target.parent_path() / (vendor + ".opc"), ec);
}
}
}
}
friend std::ostream& operator<<(std::ostream& os, const Update &self)
{
@@ -180,6 +195,8 @@ struct Updates
std::vector<Update> updates;
};
static bool reload_configs_update_gui();
wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent);
wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent);
@@ -207,6 +224,10 @@ struct PresetUpdater::priv
// Per-vendor update checking
std::set<std::string> checked_vendors;
// Orca (PR #130): changelog text for each vendor, captured in memory during
// sync_vendor_config()/check_new_vendors() instead of written beside the cache.
std::unordered_map<std::string, std::string> vendor_changelogs;
mutable std::mutex vendor_changelogs_mutex;
std::vector<std::thread> vendor_check_threads;
std::atomic<bool> vendor_check_cancel{false};
@@ -231,6 +252,8 @@ struct PresetUpdater::priv
void parse_version_string(const std::string& body) const;
void sync_resources(std::string http_url, std::map<std::string, Resource> &resources, bool check_patch = false, std::string current_version="", std::string changelog_file="");
void sync_vendor_config(const std::string& vendor_id);
void check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string>, bool)> callback);
void sync_tooltip(std::string http_url, std::string language);
void sync_plugins(std::string http_url, std::string plugin_version);
void sync_printer_config(std::string http_url);
@@ -268,7 +291,7 @@ void PresetUpdater::priv::set_download_prefs(AppConfig *app_config)
version_check_url = app_config->version_check_url();
auto profile_update_url = app_config->profile_update_url();
if (!profile_update_url.empty())
if (!profile_update_url.empty() && app_config->get_bool("enable_ota"))
enabled_config_update = true;
else
enabled_config_update = false;
@@ -671,6 +694,9 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
std::string online_version_str; // this represents the PROFILE VERSION, not ORCA VERSION
std::string download_url_str;
std::string changelog;
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] fetching vendor update status from " << url;
Http::get(url)
.timeout_connect(5)
@@ -683,9 +709,12 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (http_status != 200) return;
try {
json j = json::parse(body);
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] url: " << url << " returned:" << body;
if (j.contains("vendor_version") && j.contains("download_url")) {
online_version_str = j["vendor_version"].get<std::string>();
download_url_str = j["download_url"].get<std::string>();
changelog = j.value("changelog", std::string());
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] vendor check JSON parse failed: " << e.what();
@@ -707,6 +736,10 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
boost::system::error_code ec;
fs::remove_all(cache_profile_path / vendor_id, ec);
fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
// Orca: the OPC cache is the vendor's whole installation in one file; clear it too.
fs::remove(cache_profile_path / (vendor_id + ".opc"), ec);
// Best-effort cleanup of the legacy on-disk changelog written by older builds
// (changelogs are now kept in memory - see vendor_changelogs).
fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec);
// Download the zip
@@ -735,7 +768,7 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (!download_ok || cancel || vendor_check_cancel) return;
// Extract vendor profile bundles under ota/profiles. The downloaded zip contains
// the vendor json/folder at its root.
// either the vendor json/folder or the vendor cache at its root.
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting update for " << vendor_id;
if (!extract_file(download_file, cache_profile_path)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for " << vendor_id;
@@ -745,6 +778,27 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (cancel || vendor_check_cancel) return;
const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
const fs::path cached_vendor_opc = cache_profile_path / (vendor_id + ".opc");
bool is_json_update = fs::is_regular_file(cached_vendor_json) && fs::is_directory(cached_vendor_folder) && !fs::is_empty(cached_vendor_folder);
bool is_opc_update = fs::is_regular_file(cached_vendor_opc);
if (!is_json_update && !is_opc_update) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected update for " << vendor_id
<< ": expected " << vendor_id << ".json and a non-empty "
<< vendor_id << " directory, or OPC update format.";
fs::remove_all(cached_vendor_folder, ec);
fs::remove(cached_vendor_json, ec);
return;
}
{
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
vendor_changelogs[vendor_id] = std::move(changelog);
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] vendor " << vendor_id << " update cached, notifying UI";
GUI::wxGetApp().CallAfter([] {
GUI::wxGetApp().check_config_updates_from_updater();
@@ -1039,6 +1093,7 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:Checking whether the profile from resource is newer";
AppConfig *app_config = GUI::wxGetApp().app_config;
const auto enabled_vendors = app_config->vendors();
std::set<std::string> bundles;
@@ -1147,68 +1202,110 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
if (!fs::exists(cache_profile_path))
return updates;
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) {
const auto &path = dir_entry.path();
std::string file_path = path.string();
if (is_json_file(file_path)) {
const auto path_in_vendor = vendor_path / path.filename();
std::string vendor_name = path.filename().string();
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME);
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
// Orca (PR #130): vendor changelogs are captured in memory during
// sync_vendor_config()/check_new_vendors(), not written beside the cache.
std::unordered_map<std::string, std::string> changelogs;
{
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
changelogs = vendor_changelogs;
}
if (is_vendor_installed(vendor_name)
|| fs::exists(print_in_cache)
|| fs::exists(filament_in_cache)
|| fs::exists(machine_in_cache)) {
// Orca: a vendor installed as a preset cache carries its version there.
Semver vendor_ver = installed_vendor_version(vendor_name);
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) {
const auto &path = dir_entry.path();
std::string file_path = path.string();
const bool is_opc_file = boost::iequals(path.extension().string(), ".opc");
if (!is_json_file(file_path) && !is_opc_file)
continue;
std::map<std::string, std::string> key_values;
std::vector<std::string> keys(3);
Semver cache_ver;
keys[0] = BBL_JSON_KEY_VERSION;
keys[1] = BBL_JSON_KEY_DESCRIPTION;
keys[2] = BBL_JSON_KEY_FORCE_UPDATE;
get_values_from_json(file_path, keys, key_values);
std::string description = key_values[BBL_JSON_KEY_DESCRIPTION];
bool force_update = false;
if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end())
force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false;
auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]);
if (config_version)
cache_ver = *config_version;
const std::string vendor_name = path.stem().string();
auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME);
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
std::string changelog;
std::string changelog_file = (cache_profile_path / (vendor_name + ".changelog")).string();
boost::nowide::ifstream ifs(changelog_file);
if (ifs) {
std::ostringstream oss;
oss<< ifs.rdbuf();
changelog = oss.str();
ifs.close();
}
// Orca (PR #130): a JSON cache entry is only meaningful next to a non-empty
// <vendor>/ preset directory; a stray or half-downloaded <vendor>.json is
// skipped. An .opc cache is a single self-contained file (validated below),
// so this check does not apply to it.
if (!is_opc_file) {
const auto vendor_folder_in_cache = cache_profile_path / vendor_name;
if (!fs::is_regular_file(path) || !fs::is_directory(vendor_folder_in_cache) ||
fs::is_empty(vendor_folder_in_cache)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring invalid cached update for "
<< vendor_name << ": expected " << vendor_name
<< ".json and a non-empty " << vendor_name << " directory";
continue;
}
}
if (vendor_ver < cache_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string()
<< " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION;
Version version;
version.config_version = cache_ver;
version.comment = description;
// Orca: update vendor.json
updates.updates.emplace_back(std::move(file_path), path_in_vendor.string(), std::move(version), vendor_name, changelog, "", force_update, false);
//Orca: update vendor folder
updates.updates.emplace_back(cache_profile_path / vendor_name, vendor_path / vendor_name, Version(), vendor_name, "", "", force_update, true);
} else {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name
<< " are not newer than installed version, installed " << vendor_ver.to_string()
<< ", cached " << cache_ver.to_string();
}
}
}
}
if (is_vendor_installed(vendor_name)
|| is_opc_file
|| fs::exists(print_in_cache)
|| fs::exists(filament_in_cache)
|| fs::exists(machine_in_cache)) {
// Orca: a vendor installed as a preset cache carries its version there.
Semver vendor_ver = installed_vendor_version(vendor_name);
Semver cache_ver;
std::string description;
bool force_update = false;
if (is_opc_file) {
cache_ver = VendorCacheFile::usable_version(file_path, vendor_name);
if (!cache_ver.valid()) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring unreadable vendor cache " << file_path;
continue;
}
}
else {
std::map<std::string, std::string> key_values;
std::vector<std::string> keys(3);
keys[0] = BBL_JSON_KEY_VERSION;
keys[1] = BBL_JSON_KEY_DESCRIPTION;
keys[2] = BBL_JSON_KEY_FORCE_UPDATE;
get_values_from_json(file_path, keys, key_values);
description = key_values[BBL_JSON_KEY_DESCRIPTION];
if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end())
force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false;
auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]);
if (config_version)
cache_ver = *config_version;
}
// Orca (PR #130): changelog for this vendor was captured in memory at sync time.
const auto changelog_it = changelogs.find(vendor_name);
std::string changelog = changelog_it != changelogs.end() ? changelog_it->second : std::string();
if (vendor_ver < cache_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string()
<< " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION;
Version version;
version.config_version = cache_ver;
version.comment = description;
if (is_opc_file) {
// A cache contains the vendor profile and all presets.
// Install it directly; Update::install removes any
// superseded JSON representation.
auto &update = updates.updates.emplace_back(
std::move(file_path), vendor_path / (vendor_name + ".opc"),
std::move(version), vendor_name, changelog, "", force_update, false);
update.is_opc = true;
}
else {
// JSON profile and its preset directory are installed
// separately, as before.
updates.updates.emplace_back(std::move(file_path),
vendor_path / (vendor_name + ".json"), std::move(version),
vendor_name, changelog, "", force_update, false);
updates.updates.emplace_back(cache_profile_path / vendor_name,
vendor_path / vendor_name, Version(), vendor_name,
"", "", force_update, true);
}
} else {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name
<< " are not newer than installed version, installed " << vendor_ver.to_string()
<< ", cached " << cache_ver.to_string();
}
}
}
return updates;
}
@@ -1317,6 +1414,9 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string
// after the startup printer preset has been restored.
this->p->sync_plugins(http_url, plugin_version);
this->p->sync_printer_config(http_url);
// Orca (PR #130): the filament library is always installed, so refresh it
// from the updater on every startup sync rather than deferring to check_vendor_update().
this->p->sync_vendor_config(PresetBundle::ORCA_FILAMENT_LIBRARY);
//if (p->cancel)
// return;
//remove the tooltip currently
@@ -1349,6 +1449,302 @@ void PresetUpdater::check_vendor_update(const std::string& vendor_id)
});
}
// Orca: ask the server which vendors from `system_vendors` have a profile bundle available that
// isn't installed yet (or is newer than what's installed). Request body maps vendor id -> currently
// installed profile version (unknown/not-yet-installed vendors report "0.0.0"). Response maps
// vendor id -> {version, download_url, changelog} for each vendor the server has an update for.
// Any such vendor is downloaded and cached under ota/profiles the same way sync_vendor_config()
// does; the caller's check_config_updates_from_updater() -> get_config_updates()/perform_updates()
// flow then installs the cached profiles into data_dir()/system.
//
// Mirrors check_vendor_update()/sync_vendor_config(): the network query and the download/extract
// work run on a background thread (vendor_check_threads), never on the calling (UI) thread. Only
// the confirmation dialog (which must run on the UI thread) and the final callback are marshaled
// back via CallAfter().
void PresetUpdater::priv::check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string>, bool)> callback)
{
vendor_check_threads.emplace_back([this, system_vendors, callback]() {
AppConfig* app_config = GUI::wxGetApp().app_config;
std::string url = app_config->profile_update_url() + "/new?orcaslicer_version=" + Http::url_encode(SoftFever_VERSION);
auto check_cancel = [this](Http::Progress, bool& cancel_http) {
if (cancel || vendor_check_cancel)
cancel_http = true;
};
json request_body = json::object();
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] checking new vendors for:";
for (const auto& vendor_id : system_vendors) {
// Orca: installed_vendor_version() reads whichever form the vendor is
// installed as - the .json profile or the .opc preset cache stamp -
// so a cache-only vendor is not reported as version 0.0.0 and then
// endlessly re-offered by the server.
Semver installed_ver = installed_vendor_version(vendor_id);
request_body[vendor_id] = installed_ver.to_string();
BOOST_LOG_TRIVIAL(info) << vendor_id << " (installed version " << installed_ver.to_string() << ")";
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request url: " << url;
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request body: " << request_body.dump(2);
json response_json;
bool got_response = false;
auto post = Http::post(url);
post.timeout_connect(5);
post.on_progress(check_cancel);
post.header("Content-Type", "application/json");
post.on_error([](std::string body, std::string error, unsigned http_status) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check HTTP error: " << error;
})
.on_complete([&response_json, &got_response](std::string body, unsigned http_status) {
if (http_status != 200)
return;
try {
json j = json::parse(body);
if (j.is_object()) {
response_json = std::move(j);
got_response = true;
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check JSON parse failed: " << e.what();
}
});
post.set_post_body(request_body.dump());
post.perform_sync();
if (cancel || vendor_check_cancel)
return;
if (!got_response) {
GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
return;
}
// Collect candidates before touching the filesystem or network again.
struct NewVendorCandidate
{
std::string vendor_id;
Semver version;
std::string download_url;
std::string changelog;
};
std::vector<NewVendorCandidate> candidates;
for (auto it = response_json.begin(); it != response_json.end(); ++it) {
const json& entry = it.value();
if (!entry.is_object())
continue;
std::string download_url_str = entry.value("download_url", std::string());
if (download_url_str.empty())
continue;
NewVendorCandidate candidate;
candidate.vendor_id = it.key();
auto parsed_ver = Semver::parse(entry.value("version", std::string()));
candidate.version = parsed_ver ? *parsed_ver : Semver();
candidate.download_url = std::move(download_url_str);
candidate.changelog = entry.value("changelog", std::string());
candidates.push_back(std::move(candidate));
}
if (candidates.empty()) {
GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
return;
}
// Orca: the confirmation dialog must run on the UI thread; if confirmed, the actual
// download/install work is dispatched back onto a new background thread from there,
// same as check_vendor_update() does for a single vendor.
GUI::wxGetApp().CallAfter([this, candidates, callback]() {
std::vector<GUI::MsgUpdateConfig::Update> updates_msg;
for (const auto& candidate : candidates)
updates_msg.emplace_back(candidate.vendor_id, candidate.version, std::string(), candidate.changelog);
GUI::MsgUpdateConfig dlg(updates_msg);
if (dlg.ShowModal() != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] user declined installing new vendors";
callback({}, true);
return;
}
// Orca: the actual download runs on a background thread below (so it doesn't block the
// UI), but that also means nothing visibly happens for the several seconds it can take
// (longer still if a retry kicks in) — push a notification so it's clear work is
// ongoing rather than looking hung.
{
std::string vendor_list;
for (const auto& candidate : candidates) {
if (!vendor_list.empty())
vendor_list += ", ";
vendor_list += candidate.vendor_id;
}
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
_u8L("Downloading new vendor profile(s): ") + vendor_list + _u8L("..."));
}
vendor_check_threads.emplace_back([this, candidates, callback]() {
auto check_cancel = [this](Http::Progress, bool& cancel_http) {
if (cancel || vendor_check_cancel)
cancel_http = true;
};
std::vector<std::string> new_vendor_ids;
std::vector<std::string> failed_vendor_ids;
auto cache_profile_path = cache_path / "profiles";
fs::create_directories(cache_profile_path);
boost::system::error_code ec;
for (const auto& candidate : candidates) {
if (cancel || vendor_check_cancel)
break;
const std::string& vendor_id = candidate.vendor_id;
const std::string& download_url_str = candidate.download_url;
std::string changelog = candidate.changelog;
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading new vendor " << vendor_id << " version " << candidate.version.to_string();
// Clear only this vendor's cached data, same as sync_vendor_config().
fs::remove_all(cache_profile_path / vendor_id, ec);
fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
fs::path download_file = cache_path / (vendor_id + TMP_EXTENSION);
bool download_ok = false;
// Orca: same retry pattern as Plater.cpp's project download — a single-shot
// 5s connect timeout against GitHub's redirect chain is prone to transient
// failures (DNS/connect hiccups) that succeed a moment later, so retry a few
// times before giving up rather than failing the whole vendor on one blip.
int retry_count = 0;
const int max_retries = 3;
bool keep_trying = true;
while (keep_trying && retry_count < max_retries) {
retry_count++;
Http::get(download_url_str)
.timeout_connect(5)
.on_progress(check_cancel)
.on_error([&vendor_id, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for new vendor " << vendor_id
<< " (attempt " << retry_count << "/" << max_retries << "): " << error;
})
.on_complete([&](std::string body, unsigned http_status) {
if (http_status != 200)
return;
fs::fstream file(download_file, std::ios::out | std::ios::binary | std::ios::trunc);
if (!file.good())
return;
file.write(body.c_str(), body.size());
file.close();
if (file.good())
download_ok = true;
})
.perform_sync();
keep_trying = !download_ok && !(cancel || vendor_check_cancel);
}
if (!download_ok || cancel || vendor_check_cancel) {
if (!download_ok)
failed_vendor_ids.push_back(vendor_id);
continue;
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting new vendor " << vendor_id;
if (!extract_file(download_file, cache_profile_path)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for new vendor " << vendor_id;
fs::remove(download_file, ec);
failed_vendor_ids.push_back(vendor_id);
continue;
}
fs::remove(download_file, ec);
const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) ||
fs::is_empty(cached_vendor_folder)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected new vendor " << vendor_id << ": expected " << vendor_id
<< ".json and a non-empty " << vendor_id << " directory";
fs::remove_all(cached_vendor_folder, ec);
fs::remove(cached_vendor_json, ec);
failed_vendor_ids.push_back(vendor_id);
continue;
}
{
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
vendor_changelogs[vendor_id] = std::move(changelog);
}
new_vendor_ids.push_back(vendor_id);
}
if (!new_vendor_ids.empty()) {
// Orca: the user already confirmed via the dialog above, so install right away
// instead of routing through check_config_updates_from_updater(), which only
// queues a passive notification (meant for the silent background per-vendor
// check) requiring yet another click + confirmation before anything is copied
// into data_dir()/system.
GUI::wxGetApp().CallAfter([this, new_vendor_ids] {
AppConfig* app_config = GUI::wxGetApp().app_config;
Updates updates = get_config_updates(app_config->orig_version());
// Only install the vendors just confirmed; leave any other unrelated
// pending cached update (from a background sync_vendor_config()) alone,
// still gated behind its own notification/confirmation.
std::set<std::string> confirmed(new_vendor_ids.begin(), new_vendor_ids.end());
Updates filtered;
for (auto& update : updates.updates)
if (confirmed.count(update.vendor))
filtered.updates.push_back(std::move(update));
if (filtered.updates.empty()) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendors cached but no updates detected";
return;
}
if (!perform_updates(std::move(filtered)) || !reload_configs_update_gui()) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] failed to install new vendors";
return;
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendors installed";
for (const auto& vendor_id : new_vendor_ids) {
Semver cur_ver = GUI::wxGetApp().preset_bundle->get_vendor_profile_version(vendor_id);
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
GUI::NotificationType::PresetUpdateFinished,
GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel,
_u8L("Configuration package: ") + vendor_id + _u8L(" updated to ") + cur_ver.to_string());
}
});
}
if (!failed_vendor_ids.empty()) {
GUI::wxGetApp().CallAfter([failed_vendor_ids] {
std::string vendor_list;
for (const auto& vendor_id : failed_vendor_ids) {
if (!vendor_list.empty())
vendor_list += ", ";
vendor_list += vendor_id;
}
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
_u8L("Failed to download vendor profile(s): ") + vendor_list);
});
}
GUI::wxGetApp().CallAfter([callback, new_vendor_ids]() { callback(new_vendor_ids, false); });
});
});
});
}
void PresetUpdater::check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string>, bool)> callback)
{
p->check_new_vendors(system_vendors, std::move(callback));
}
void PresetUpdater::slic3r_update_notify()
{
if (! p->enabled_version_check)
@@ -1370,10 +1766,9 @@ static bool reload_configs_update_gui()
GUI::wxGetApp().load_current_presets();
GUI::wxGetApp().plater()->set_bed_shape();
return true;
return true;
}
PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params) const
{
if (! p->enabled_config_update) { return R_NOOP; }
+8
View File
@@ -1,7 +1,9 @@
#ifndef slic3r_PresetUpdate_hpp_
#define slic3r_PresetUpdate_hpp_
#include <functional>
#include <memory>
#include <set>
#include <vector>
#include <wx/event.h>
@@ -59,6 +61,12 @@ public:
void on_update_notification_confirm();
void do_printer_config_update();
void check_vendor_update(const std::string& vendor_id);
// Orca: async, mirrors check_vendor_update()/sync_vendor_config() — the network query and any
// download/install work happen on a background thread; only the confirmation dialog runs on
// the UI thread. `callback` is invoked on the UI thread with the ids of vendors that were
// installed (empty if none were found, or the user declined) and whether the user declined.
void check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string> installed_vendors, bool declined)> callback);
bool version_check_enabled() const;