mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Merge branch 'main' into plugin-ui-1
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include <boost/nowide/cstdio.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include "stackwalker.h"
|
||||
#include "StackWalker.h"
|
||||
#include <eh.h>
|
||||
|
||||
class CBaseException : public CStackWalker
|
||||
|
||||
@@ -425,7 +425,7 @@ LPSTACKINFO CStackWalker::StackWalker(HANDLE hThread, const CONTEXT* context)
|
||||
else
|
||||
c = *context;
|
||||
|
||||
STACKFRAME64 sf = {0};
|
||||
STACKFRAME64 sf = {};
|
||||
DWORD imageType;
|
||||
|
||||
//intel X86
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -372,6 +372,8 @@ set(lisbslic3r_sources
|
||||
Preset.hpp
|
||||
PrincipalComponents2D.cpp
|
||||
PrincipalComponents2D.hpp
|
||||
PublishSettings.cpp
|
||||
PublishSettings.hpp
|
||||
PrintApply.cpp
|
||||
PrintBase.cpp
|
||||
PrintBase.hpp
|
||||
|
||||
+36
-39
@@ -11,7 +11,7 @@
|
||||
|
||||
#include "AABBTreeLines.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "FillBase.hpp"
|
||||
#include "Fill.hpp"
|
||||
#include "FillRectilinear.hpp"
|
||||
#include "FillLightning.hpp"
|
||||
#include "FillConcentricInternal.hpp"
|
||||
@@ -1234,6 +1234,33 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
return surface_fills;
|
||||
}
|
||||
|
||||
// Orca: Anchors and printed infill must share the same body origin. Keep the choice
|
||||
// here so per-model surface centering and separated sparse infill cannot drift apart.
|
||||
static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox)
|
||||
{
|
||||
const auto ¶ms = fill.params;
|
||||
const auto &config = layer.regions()[fill.region_id]->region().config();
|
||||
const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
|
||||
const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
|
||||
(params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral);
|
||||
const bool separate = !external && params.separated_infills &&
|
||||
(is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() ||
|
||||
!config.sparse_infill_rotate_template.value.empty());
|
||||
if (per_model || separate) {
|
||||
double best_overlap = 0.;
|
||||
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) {
|
||||
const double overlap = area(intersection_ex(layer.lslices[i], expoly));
|
||||
if (overlap > best_overlap) {
|
||||
best_overlap = overlap;
|
||||
const Point center = layer.lslices_separated_component_bboxes[i].center();
|
||||
bbox = layer.object()->bounding_box();
|
||||
bbox.translate(center.x(), center.y());
|
||||
}
|
||||
}
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill> &fills)
|
||||
{
|
||||
@@ -1353,19 +1380,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
|
||||
// Orca: Checking the filling of a centered surface by drawing for each model parts
|
||||
bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
|
||||
bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral;
|
||||
if (is_top_or_bottom) {
|
||||
params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern
|
||||
}
|
||||
// Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly
|
||||
// fall through to the default (whole-object) bounding box below.
|
||||
bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill;
|
||||
bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills &&
|
||||
(
|
||||
is_separable_infill_pattern(surface_fill.params.pattern) ||
|
||||
params.config->solid_infill_rotate_template != "" ||
|
||||
params.config->sparse_infill_rotate_template != "" );
|
||||
|
||||
if( surface_fill.params.pattern == ipLockedZag ) {
|
||||
params.locked_zag = true;
|
||||
params.infill_lock_depth = surface_fill.params.infill_lock_depth;
|
||||
@@ -1389,34 +1406,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.can_reverse = false;
|
||||
for (ExPolygon& expoly : surface_fill.expolygons) {
|
||||
|
||||
// Orca: separate infill / per-model pattern centering.
|
||||
//
|
||||
// Center the pattern on each connected body of the object independently, so every piece
|
||||
// is filled exactly as if it were sliced on its own: touching/overlapping parts merge
|
||||
// into one body sharing a center, while separate parts and disconnected islands (even
|
||||
// interleaved-but-not-touching ones, e.g. chain links) each get their own. The body each
|
||||
// island belongs to, and its full bounding box, were resolved in 3D by PrintObject::
|
||||
// infill() (lslices_separated_component_bboxes, aligned with this layer's lslices). We
|
||||
// match this fill region to the island it overlaps most, then re-use the whole-object
|
||||
// bounding box (origin-centered — identical extent to the default, so coverage and cost
|
||||
// are unchanged) re-centered on that body.
|
||||
if (is_per_model_center || is_separate_infill) {
|
||||
double best_overlap = 0.;
|
||||
BoundingBox best_component;
|
||||
for (size_t r = 0; r < this->lslices.size() && r < this->lslices_separated_component_bboxes.size(); ++ r) {
|
||||
const double overlap = area(intersection_ex(this->lslices[r], expoly));
|
||||
if (overlap > best_overlap) {
|
||||
best_overlap = overlap;
|
||||
best_component = this->lslices_separated_component_bboxes[r];
|
||||
}
|
||||
}
|
||||
if (best_component.defined) {
|
||||
const Point c = best_component.center();
|
||||
BoundingBox part_bbox = bbox; // origin-centered, whole-object extent (from above)
|
||||
part_bbox.translate(c.x(), c.y()); // re-center on this body
|
||||
f->set_bounding_box(part_bbox);
|
||||
}
|
||||
} // - End: separate infill / per-model pattern centering
|
||||
// Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface.
|
||||
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
|
||||
|
||||
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
if (params.symmetric_infill_y_axis) {
|
||||
@@ -1583,8 +1574,14 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
|
||||
// Without the sparse extrusion role, the filler uses each surface's bounds
|
||||
// instead of the object's bounds, so bridge anchors shift away from printed infill.
|
||||
params.extrusion_role = surface_fill.params.extrusion_role;
|
||||
|
||||
for (ExPolygon &expoly : surface_fill.expolygons) {
|
||||
// Orca: Match the per-body origin of make_fills() before generating physical anchors.
|
||||
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
|
||||
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
||||
f->spacing = surface_fill.params.spacing;
|
||||
surface_fill.surface.expolygon = std::move(expoly);
|
||||
|
||||
@@ -14,6 +14,12 @@ namespace Slic3r {
|
||||
|
||||
class ExtrusionEntityCollection;
|
||||
class LayerRegion;
|
||||
class PrintObject;
|
||||
|
||||
// Orca: Share the layer rotation calculation between infill generation and internal
|
||||
// bridge angle selection so both interpret rotation templates in the same way.
|
||||
double calculate_infill_rotation_angle(const PrintObject *object, size_t layer_id,
|
||||
const double &fixed_infill_angle, const std::string &template_string);
|
||||
|
||||
// An interface class to Perl, aggregating an instance of a Fill and a FillData.
|
||||
class Filler
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -8795,7 +8844,7 @@ public:
|
||||
auto model = object.get_model();
|
||||
auto o = m_temp_model.add_object(object);
|
||||
int backup_id = model->get_object_backup_id(object);
|
||||
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, 1 });
|
||||
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, { 1 } });
|
||||
}
|
||||
|
||||
void remove_object_mesh(ModelObject& object) {
|
||||
@@ -8805,7 +8854,7 @@ public:
|
||||
void backup_soon() {
|
||||
boost::lock_guard lock(m_mutex);
|
||||
m_other_changes_backup = true;
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
|
||||
m_cond.notify_all();
|
||||
}
|
||||
|
||||
@@ -8823,7 +8872,7 @@ public:
|
||||
m_ui_tasks.clear();
|
||||
m_tasks.clear();
|
||||
}
|
||||
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, removeAll });
|
||||
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, { removeAll } });
|
||||
++m_task_seq;
|
||||
if (model.is_need_backup()) {
|
||||
m_other_changes = false;
|
||||
@@ -9038,7 +9087,7 @@ public:
|
||||
else
|
||||
m_cond.wait(lock);
|
||||
if (m_interval > 0 && boost::get_system_time() > m_next_backup) {
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
|
||||
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
|
||||
m_next_backup += boost::posix_time::seconds(m_interval);
|
||||
// Maybe wakeup from power sleep
|
||||
if (m_next_backup < boost::get_system_time())
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1437
-145
File diff suppressed because it is too large
Load Diff
@@ -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);*/
|
||||
|
||||
|
||||
@@ -5430,7 +5430,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->readonly = false;
|
||||
def->nullable = true;
|
||||
def->set_default_value(new ConfigOptionFloatsNullable { {0.0} });
|
||||
def->set_default_value(new ConfigOptionFloatsNullable { 0.0 });
|
||||
|
||||
def = this->add("cooling_tube_retraction", coFloat);
|
||||
def->label = L("Cooling tube position");
|
||||
|
||||
+186
-118
@@ -21,9 +21,11 @@
|
||||
#include "TriangleMeshSlicer.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "Fill/FillAdaptive.hpp"
|
||||
#include "Fill/Fill.hpp"
|
||||
#include "Fill/FillLightning.hpp"
|
||||
#include "Format/STL.hpp"
|
||||
#include "format.hpp"
|
||||
#include "AABBTreeIndirect.hpp"
|
||||
#include "AABBTreeLines.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -672,6 +674,98 @@ void PrintObject::prepare_infill()
|
||||
} // for each region
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
|
||||
// Orca: precompute the object's 3D connected bodies for separated infills / per-model
|
||||
// centering. Two islands belong to the same body when their slices overlap on adjacent
|
||||
// layers; islands that only overlap in top-down projection but never touch (e.g. interleaved
|
||||
// chain links) stay separate, matching "split to objects". Each layer island then records
|
||||
// the full bounding box of its body, so its infill is centered on that body as if it were
|
||||
// sliced alone. Compute this before bridges so anchors and extrusion share the same origin.
|
||||
bool needs_separated_components = false;
|
||||
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
|
||||
const PrintRegionConfig &rc = this->printing_region(i).config();
|
||||
if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) {
|
||||
needs_separated_components = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Orca: Fast path: the feature only changes anything when the object is made of more than one
|
||||
// connected body. Detect that cheaply the same way as "Split to objects" — more than one
|
||||
// model part, or a single part whose mesh is splittable (is_splittable() is cached). A single
|
||||
// body already shares the object center, i.e. the default, so skip the connectivity pass.
|
||||
if (needs_separated_components) {
|
||||
int parts = 0;
|
||||
const ModelVolume *first_part = nullptr;
|
||||
for (const ModelVolume *v : this->model_object()->volumes)
|
||||
if (v->is_model_part()) { ++ parts; first_part = v; }
|
||||
if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable()))
|
||||
needs_separated_components = false;
|
||||
}
|
||||
for (Layer *layer : m_layers)
|
||||
layer->lslices_separated_component_bboxes.clear();
|
||||
if (needs_separated_components) {
|
||||
const size_t nl = m_layers.size();
|
||||
std::vector<size_t> offset(nl + 1, 0); // Orca: flat index of the first island of each layer
|
||||
for (size_t i = 0; i < nl; ++ i)
|
||||
offset[i + 1] = offset[i] + m_layers[i]->lslices.size();
|
||||
const size_t nreg = offset[nl];
|
||||
// Orca: Union-find over every (layer, island).
|
||||
std::vector<size_t> parent(nreg);
|
||||
for (size_t i = 0; i < nreg; ++ i) parent[i] = i;
|
||||
auto find = [&parent](size_t x) {
|
||||
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
|
||||
return x;
|
||||
};
|
||||
auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; };
|
||||
// Orca: Index the smaller of two consecutive layers instead of scanning every
|
||||
// pair of islands. The tree prunes distant boxes on fragmented models; exact
|
||||
// polygon intersections still decide connectivity for the remaining candidates.
|
||||
for (size_t i = 0; i + 1 < nl; ++ i) {
|
||||
m_print->throw_if_canceled();
|
||||
size_t layer_a = i, layer_b = i + 1;
|
||||
if (m_layers[layer_a]->lslices.size() < m_layers[layer_b]->lslices.size())
|
||||
std::swap(layer_a, layer_b);
|
||||
const Layer *la = m_layers[layer_a], *lb = m_layers[layer_b];
|
||||
if (lb->lslices.empty())
|
||||
continue;
|
||||
|
||||
using IslandTree = AABBTreeIndirect::Tree<2, coord_t>;
|
||||
std::vector<AABBTreeIndirect::BoundingBoxWrapper> bboxes;
|
||||
bboxes.reserve(lb->lslices.size());
|
||||
for (size_t b = 0; b < lb->lslices.size(); ++ b)
|
||||
bboxes.emplace_back(b, lb->lslices_bboxes[b]);
|
||||
IslandTree tree;
|
||||
tree.build_modify_input(bboxes);
|
||||
for (size_t a = 0; a < la->lslices.size(); ++ a) {
|
||||
const IslandTree::BoundingBox query(la->lslices_bboxes[a].min, la->lslices_bboxes[a].max);
|
||||
AABBTreeIndirect::traverse(tree,
|
||||
[&query](const IslandTree::Node &node) { return node.bbox.intersects(query); },
|
||||
[&](const IslandTree::Node &node) {
|
||||
const size_t b = node.idx;
|
||||
// Orca: Tree boxes include an epsilon, so retain the original box
|
||||
// filter. Already-connected islands cannot change the partition
|
||||
// and need no further polygon intersection.
|
||||
if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) &&
|
||||
find(offset[layer_a] + a) != find(offset[layer_b] + b) &&
|
||||
! intersection_ex(la->lslices[a], lb->lslices[b]).empty())
|
||||
unite(offset[layer_a] + a, offset[layer_b] + b);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
// Orca: Full bounding box of each body, indexed by its union-find root.
|
||||
std::vector<BoundingBox> body_bbox(nreg);
|
||||
for (size_t i = 0; i < nl; ++ i)
|
||||
for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a)
|
||||
body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]);
|
||||
// Orca: Store the body bbox for every island.
|
||||
for (size_t i = 0; i < nl; ++ i) {
|
||||
Layer *layer = m_layers[i];
|
||||
layer->lslices_separated_component_bboxes.resize(layer->lslices.size());
|
||||
for (size_t a = 0; a < layer->lslices.size(); ++ a)
|
||||
layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)];
|
||||
}
|
||||
}
|
||||
|
||||
// the following step needs to be done before combination because it may need
|
||||
// to remove only half of the combined infill
|
||||
this->bridge_over_infill();
|
||||
@@ -706,71 +800,6 @@ void PrintObject::infill()
|
||||
if (this->set_started(posInfill)) {
|
||||
m_print->set_status(35, L("Generating infill toolpath"));
|
||||
|
||||
// Orca: precompute the object's 3D connected bodies for separated infills / per-model
|
||||
// centering. Two islands belong to the same body when their slices overlap on adjacent
|
||||
// layers; islands that only overlap in top-down projection but never touch (e.g. interleaved
|
||||
// chain links) stay separate, matching "split to objects". Each layer island then records
|
||||
// the full bounding box of its body, so its infill is centered on that body as if it were
|
||||
// sliced alone. Done once here, before the parallel fill, and only when a region needs it.
|
||||
bool needs_separated_components = false;
|
||||
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
|
||||
const PrintRegionConfig &rc = this->printing_region(i).config();
|
||||
if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) {
|
||||
needs_separated_components = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fast path: the feature only changes anything when the object is made of more than one
|
||||
// connected body. Detect that cheaply the same way as "Split to objects" — more than one
|
||||
// model part, or a single part whose mesh is splittable (is_splittable() is cached). A single
|
||||
// body already shares the object center, i.e. the default, so skip the connectivity pass.
|
||||
if (needs_separated_components) {
|
||||
int parts = 0;
|
||||
const ModelVolume *first_part = nullptr;
|
||||
for (const ModelVolume *v : this->model_object()->volumes)
|
||||
if (v->is_model_part()) { ++ parts; first_part = v; }
|
||||
if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable()))
|
||||
needs_separated_components = false;
|
||||
}
|
||||
for (Layer *layer : m_layers)
|
||||
layer->lslices_separated_component_bboxes.clear();
|
||||
if (needs_separated_components) {
|
||||
const size_t nl = m_layers.size();
|
||||
std::vector<size_t> offset(nl + 1, 0); // flat index of the first island of each layer
|
||||
for (size_t i = 0; i < nl; ++ i)
|
||||
offset[i + 1] = offset[i] + m_layers[i]->lslices.size();
|
||||
const size_t nreg = offset[nl];
|
||||
// Union-find over every (layer, island).
|
||||
std::vector<size_t> parent(nreg);
|
||||
for (size_t i = 0; i < nreg; ++ i) parent[i] = i;
|
||||
auto find = [&parent](size_t x) {
|
||||
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
|
||||
return x;
|
||||
};
|
||||
auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; };
|
||||
// Join islands that overlap between two consecutive layers.
|
||||
for (size_t i = 0; i + 1 < nl; ++ i) {
|
||||
const Layer *la = m_layers[i], *lb = m_layers[i + 1];
|
||||
for (size_t a = 0; a < la->lslices.size(); ++ a)
|
||||
for (size_t b = 0; b < lb->lslices.size(); ++ b)
|
||||
if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) &&
|
||||
! intersection_ex(la->lslices[a], lb->lslices[b]).empty())
|
||||
unite(offset[i] + a, offset[i + 1] + b);
|
||||
}
|
||||
// Full bounding box of each body, indexed by its union-find root.
|
||||
std::vector<BoundingBox> body_bbox(nreg);
|
||||
for (size_t i = 0; i < nl; ++ i)
|
||||
for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a)
|
||||
body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]);
|
||||
// Store the body bbox for every island.
|
||||
for (size_t i = 0; i < nl; ++ i) {
|
||||
Layer *layer = m_layers[i];
|
||||
layer->lslices_separated_component_bboxes.resize(layer->lslices.size());
|
||||
for (size_t a = 0; a < layer->lslices.size(); ++ a)
|
||||
layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)];
|
||||
}
|
||||
}
|
||||
|
||||
const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first;
|
||||
const auto& support_fill_octree = this->m_adaptive_fill_octrees.second;
|
||||
|
||||
@@ -1401,8 +1430,6 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_anchor_max"
|
||||
|| opt_key == "top_surface_line_width"
|
||||
|| opt_key == "bottom_surface_density"
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
|| opt_key == "separated_infills"
|
||||
|| opt_key == "initial_layer_line_width"
|
||||
|| opt_key == "small_area_infill_flow_compensation"
|
||||
|| opt_key == "lateral_lattice_angle_1"
|
||||
@@ -1410,6 +1437,10 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_overhang_angle") {
|
||||
steps.emplace_back(posInfill);
|
||||
} else if (opt_key == "sparse_infill_pattern"
|
||||
// Orca: Body centering now also determines bridge anchors during preparation.
|
||||
// Invalidating preparation also invalidates infill, including top/bottom surfaces.
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
|| opt_key == "separated_infills"
|
||||
|| opt_key == "sparse_infill_smooth_factor"
|
||||
|| opt_key == "symmetric_infill_y_axis"
|
||||
|| opt_key == "infill_shift_step"
|
||||
@@ -3009,21 +3040,12 @@ void PrintObject::bridge_over_infill()
|
||||
return diff(layers_sparse_infill, not_sparse_infill);
|
||||
};
|
||||
|
||||
// LAMBDA do determine optimal bridging angle
|
||||
auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors, InfillPattern dominant_pattern, double infill_direction) {
|
||||
// Orca: Derive the fallback bridge direction from the supplied anchor geometry.
|
||||
// Pattern-specific angle selection belongs at the call site, where the supporting
|
||||
// layer and region are known; this helper must not override it with a base config angle.
|
||||
auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors) {
|
||||
AABBTreeLines::LinesDistancer<Line> lines_tree(anchors);
|
||||
|
||||
// Orca: since 3D Honeycomb was "fixed" by forcing coordf_t layerHeight = scale_(1.0), this is no longer needed.
|
||||
// CorssHatch also does not need fixed angle.
|
||||
//
|
||||
// Check it the infill that require a fixed infill angle.
|
||||
//switch (dominant_pattern) {
|
||||
//case ip3DHoneycomb:
|
||||
//case ipCrossHatch:
|
||||
// return (infill_direction + 45.0) * 2.0 * M_PI / 360.;
|
||||
//default: break;
|
||||
//}
|
||||
|
||||
std::map<double, int> counted_directions;
|
||||
for (const Polygon &p : bridged_area) {
|
||||
double acc_distance = 0;
|
||||
@@ -3089,18 +3111,15 @@ void PrintObject::bridge_over_infill()
|
||||
if (bridging_angle == 0) {
|
||||
bridging_angle = 0.001;
|
||||
}
|
||||
switch (dominant_pattern) {
|
||||
case ipHilbertCurve: bridging_angle += 0.25 * PI; break;
|
||||
case ipOctagramSpiral: bridging_angle += (1.0 / 16.0) * PI; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return bridging_angle;
|
||||
};
|
||||
|
||||
// LAMBDA that will fill given polygons with lines, exapand the lines to the nearest anchor, and reconstruct polygons from the newly
|
||||
// generated lines
|
||||
auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle) {
|
||||
// Orca: Extend scan sections to the nearest anchors and reconstruct the bridge area.
|
||||
// scan_spacing controls boundary sampling independently of the extrusion spacing;
|
||||
// anchoring overlap and smoothing thresholds still use the physical bridging flow.
|
||||
auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle,
|
||||
coord_t scan_spacing, bool restore_anchors = false) {
|
||||
auto lines_rotate = [](Lines &lines, double cos_angle, double sin_angle) {
|
||||
for (Line &l : lines) {
|
||||
double ax = double(l.a.x());
|
||||
@@ -3127,12 +3146,12 @@ void PrintObject::bridge_over_infill()
|
||||
BoundingBox bb_x = get_extents(bridged_area);
|
||||
BoundingBox bb_y = get_extents(anchors);
|
||||
|
||||
const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + bridging_flow.scaled_spacing() - 1) / bridging_flow.scaled_spacing();
|
||||
const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + scan_spacing - 1) / scan_spacing;
|
||||
std::vector<Line> vertical_lines(n_vlines);
|
||||
for (size_t i = 0; i < n_vlines; i++) {
|
||||
// Orca: Make sure the line is placed in the middle of the extrusion
|
||||
// coord_t x = bb_x.min.x() + i * bridging_flow.scaled_spacing();
|
||||
coord_t x = bb_x.min.x() + (i + 0.5) * bridging_flow.scaled_spacing();
|
||||
// Orca: Sample the center of each reconstructed strip. Its edges lie
|
||||
// half a scan step away, even when the sampling is finer than extrusion.
|
||||
coord_t x = bb_x.min.x() + (i + 0.5) * scan_spacing;
|
||||
coord_t y_min = bb_y.min.y() - bridging_flow.scaled_spacing();
|
||||
coord_t y_max = bb_y.max.y() + bridging_flow.scaled_spacing();
|
||||
vertical_lines[i].a = Point{x, y_min};
|
||||
@@ -3155,7 +3174,11 @@ void PrintObject::bridge_over_infill()
|
||||
auto anchors_intersections = anchors_and_walls_tree.intersections_with_line<true>(vertical_lines[i]);
|
||||
|
||||
for (Line §ion : polygon_sections[i]) {
|
||||
auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a,
|
||||
// Orca: A repaired boundary may already overlap its anchor by one flow width.
|
||||
// Include that overlap in the search so restoring rounded corners does not
|
||||
// extend every already anchored section into the next sparse infill cell.
|
||||
const coord_t overlap = restore_anchors ? bridging_flow.scaled_width() + SCALED_EPSILON : 0;
|
||||
auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a + Point{0, overlap},
|
||||
[](const Point &a, const std::pair<Point, size_t> &b) {
|
||||
return a.y() > b.first.y();
|
||||
});
|
||||
@@ -3164,7 +3187,7 @@ void PrintObject::bridge_over_infill()
|
||||
section.a.y() -= bridging_flow.scaled_width() * (0.5 + 0.5);
|
||||
}
|
||||
|
||||
auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b,
|
||||
auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b - Point{0, overlap},
|
||||
[](const Point &a, const std::pair<Point, size_t> &b) {
|
||||
return a.y() < b.first.y();
|
||||
});
|
||||
@@ -3194,7 +3217,9 @@ void PrintObject::bridge_over_infill()
|
||||
});
|
||||
}
|
||||
|
||||
// reconstruct polygon from polygon sections
|
||||
// Orca: Reconstruct the polygon from scan sections. At discontinuities and
|
||||
// strip starts/ends, use half the scan step for the X offsets; using half an
|
||||
// extrusion spacing would overlap the finer strips and distort curved anchors.
|
||||
struct TracedPoly
|
||||
{
|
||||
Points lows;
|
||||
@@ -3220,8 +3245,8 @@ void PrintObject::bridge_over_infill()
|
||||
36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) {
|
||||
traced_poly.lows.push_back(candidate->a);
|
||||
} else {
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a - Point{scan_spacing / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a);
|
||||
}
|
||||
|
||||
@@ -3229,8 +3254,8 @@ void PrintObject::bridge_over_infill()
|
||||
36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) {
|
||||
traced_poly.highs.push_back(candidate->b);
|
||||
} else {
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b - Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b);
|
||||
}
|
||||
segment_added = true;
|
||||
@@ -3238,9 +3263,9 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
|
||||
if (!segment_added) {
|
||||
// Zero overlapping segments, we just close this polygon
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
// Orca: No section continues this strip; close at its right edge.
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0});
|
||||
Polygon &new_poly = expanded_bridged_area.emplace_back(std::move(traced_poly.lows));
|
||||
new_poly.points.insert(new_poly.points.end(), traced_poly.highs.rbegin(), traced_poly.highs.rend());
|
||||
traced_poly.lows.clear();
|
||||
@@ -3255,9 +3280,9 @@ void PrintObject::bridge_over_infill()
|
||||
for (const auto &segment : polygon_slice) {
|
||||
if (used_segments.find(&segment) == used_segments.end()) {
|
||||
TracedPoly &new_tp = current_traced_polys.emplace_back();
|
||||
new_tp.lows.push_back(segment.a - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
new_tp.lows.push_back(segment.a - Point{scan_spacing / 2, 0});
|
||||
new_tp.lows.push_back(segment.a);
|
||||
new_tp.highs.push_back(segment.b - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
new_tp.highs.push_back(segment.b - Point{scan_spacing / 2, 0});
|
||||
new_tp.highs.push_back(segment.b);
|
||||
}
|
||||
}
|
||||
@@ -3364,7 +3389,10 @@ void PrintObject::bridge_over_infill()
|
||||
total_fill_area = closing(total_fill_area, float(SCALED_EPSILON));
|
||||
expansion_area = closing(expansion_area, float(SCALED_EPSILON));
|
||||
expansion_area = intersection(expansion_area, deep_infill_area);
|
||||
Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing));
|
||||
// Orca: Preserve the real lower-layer anchors for every candidate in this
|
||||
// layer. Replacing this shared set for one pattern also changes later regions,
|
||||
// and synthetic straight lines can claim support where no infill is printed.
|
||||
const Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing));
|
||||
Polygons internal_unsupported_area = shrink(deep_infill_area, spacing * 4.5);
|
||||
|
||||
#ifdef DEBUG_BRIDGE_OVER_INFILL
|
||||
@@ -3375,6 +3403,9 @@ void PrintObject::bridge_over_infill()
|
||||
std::vector<CandidateSurface> expanded_surfaces;
|
||||
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
|
||||
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
|
||||
region_config.sparse_infill_pattern == ipOctagramSpiral;
|
||||
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
|
||||
Polygons area_to_be_bridge = expand(candidate.new_polys, flow.scaled_spacing());
|
||||
area_to_be_bridge = intersection(area_to_be_bridge, deep_infill_area);
|
||||
@@ -3403,20 +3434,40 @@ void PrintObject::bridge_over_infill()
|
||||
to_lines(area_to_be_bridge), to_lines(boundary_plines), to_lines(anchors), to_lines(expansion_area));
|
||||
#endif
|
||||
|
||||
double bridging_angle = 0;
|
||||
if (!anchors.empty()) {
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors),
|
||||
candidate.region->region().config().sparse_infill_pattern.value,
|
||||
candidate.region->region().config().infill_direction.value);
|
||||
} else {
|
||||
// use expansion boundaries as anchors.
|
||||
// Also, use Infill pattern that is neutral for angle determination, since there are no infill lines.
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(boundary_plines), InfillPattern::ipLine, 0);
|
||||
double bridging_angle = -1.;
|
||||
if (!anchors.empty() && turning_pattern) {
|
||||
// Orca: Keep adjacent bridges over Hilbert/Octagram aligned despite
|
||||
// their many local turning directions. Use the lower layer's rotation,
|
||||
// since that is the infill supporting the bridge, not the current layer's.
|
||||
for (const LayerRegion *lower_region : layer->lower_layer->regions()) {
|
||||
// Orca: Apply the configured direction only if the same region has
|
||||
// sparse infill below this bridge. A height modifier may put another
|
||||
// pattern underneath, requiring the geometry-based fallback below.
|
||||
if (&lower_region->region() != &candidate.region->region() ||
|
||||
intersection(area_to_be_bridge, to_polygons(lower_region->fill_surfaces.filter_by_type(stInternal))).empty())
|
||||
continue;
|
||||
bridging_angle = calculate_infill_rotation_angle(po, layer->lower_layer->id(), region_config.infill_direction.value,
|
||||
region_config.sparse_infill_rotate_template.value) + 0.5 * PI;
|
||||
// Orca: Apply model alignment as infill generation does, then normalize
|
||||
// the undirected bridge angle to [0, PI), including negative rotations.
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
const auto &m = po->trafo().matrix();
|
||||
bridging_angle += std::atan2(double(m(1, 0)), double(m(0, 0)));
|
||||
}
|
||||
bridging_angle = std::fmod(bridging_angle, PI);
|
||||
if (bridging_angle < 0.)
|
||||
bridging_angle += PI;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Orca: A different region below (e.g. a height modifier) needs the actual anchor
|
||||
// directions. When there are no sparse anchors, use the expansion boundaries.
|
||||
if (bridging_angle < 0.)
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors.empty() ? boundary_plines : anchors));
|
||||
|
||||
// ORCA: Internal bridge angle override
|
||||
// Orca: Preserve the user's absolute or relative internal bridge angle
|
||||
// override after automatic direction selection.
|
||||
if (candidate.region->region().config().internal_bridge_angle.value > 0) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const double custom_angle_rad = Geometry::deg2rad(region_config.internal_bridge_angle.value);
|
||||
if (region_config.relative_bridge_angle.value)
|
||||
bridging_angle += custom_angle_rad;
|
||||
@@ -3429,11 +3480,19 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: Changing the bridge direction must not change its physical supports.
|
||||
// Extend to actual sparse infill or the existing boundary anchors, never to
|
||||
// a synthetic grid that merely has the same nominal angle and spacing.
|
||||
boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end());
|
||||
if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) {
|
||||
boundary_plines = intersection_pl(boundary_plines, expand(area_to_be_bridge, scale_(10)));
|
||||
}
|
||||
Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle);
|
||||
// Orca: Use four samples per extrusion spacing for Hilbert/Octagram so the
|
||||
// reconstructed boundary follows rounded anchors instead of cutting corners.
|
||||
// Keep the original step for other patterns and at least one coordinate unit
|
||||
// after integer division. This changes boundary accuracy, not infill density.
|
||||
const coord_t scan_spacing = std::max(coord_t(1), flow.scaled_spacing() / (turning_pattern ? 4 : 1));
|
||||
Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing);
|
||||
|
||||
// Check collision with other expanded surfaces
|
||||
{
|
||||
@@ -3447,7 +3506,9 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
}
|
||||
if (reconstruct) {
|
||||
bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle);
|
||||
// Orca: Retain the same sampling accuracy when matching a nearby
|
||||
// bridge's direction; rebuilding must not lose the curved supports.
|
||||
bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3455,6 +3516,13 @@ void PrintObject::bridge_over_infill()
|
||||
// bridging_area = opening(bridging_area, flow.scaled_spacing());
|
||||
bridging_area = opening(bridging_area, flow.scaled_spacing() * 0.75);
|
||||
bridging_area = closing(bridging_area, flow.scaled_spacing());
|
||||
// Orca: Opening/closing can pull rounded bridge ends away from their real
|
||||
// supports. Restore those contacts after smoothing, preserving the cleaned
|
||||
// area and the selected angle; do not smooth the restored contacts again.
|
||||
if (turning_pattern && !bridging_area.empty()) {
|
||||
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
|
||||
bridging_angle, scan_spacing, true));
|
||||
}
|
||||
bridging_area = intersection(bridging_area, limiting_area);
|
||||
bridging_area = intersection(bridging_area, total_fill_area);
|
||||
bridging_area = diff(bridging_area, total_top_area);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -451,6 +453,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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -627,8 +627,10 @@ private:
|
||||
void on_button_click(wxCommandEvent &WXUNUSED(ev));
|
||||
void save_colors_to_config();
|
||||
private:
|
||||
#if !defined(__linux__) && !defined(__LINUX__)
|
||||
wxColourData* m_clrData{nullptr};
|
||||
wxColourPickerWidget* m_picker_widget{nullptr};
|
||||
#endif
|
||||
};
|
||||
|
||||
class PointCtrl : public Field {
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -598,7 +598,7 @@ wxString file_wildcards(FileType file_type, const std::string &custom_extension)
|
||||
static std::string libslic3r_translate_callback(const char *s) { return wxGetTranslation(wxString(s, wxConvUTF8)).utf8_str().data(); }
|
||||
|
||||
#ifdef WIN32
|
||||
static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 };
|
||||
static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } };
|
||||
|
||||
static void register_win32_device_notification_event()
|
||||
{
|
||||
|
||||
@@ -69,6 +69,8 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std
|
||||
HANDLE handlesrc = nullptr;
|
||||
HANDLE handledst = nullptr;
|
||||
CopyFileResult ret = SUCCESS;
|
||||
DWORD size = 0;
|
||||
DWORD dwRead = 0, dwWrite = 0;
|
||||
|
||||
handlesrc = CreateFile(src.wc_str(),
|
||||
GENERIC_READ,
|
||||
@@ -96,9 +98,8 @@ CopyFileResult copy_file_gui(const std::string &from, const std::string &to, std
|
||||
goto __finished;
|
||||
}
|
||||
|
||||
DWORD size=GetFileSize(handlesrc,NULL);
|
||||
size = GetFileSize(handlesrc,NULL);
|
||||
buff = new char[size+1];
|
||||
DWORD dwRead=0,dwWrite;
|
||||
result = ReadFile(handlesrc, buff, size, &dwRead, NULL);
|
||||
if (!result) {
|
||||
DWORD errCode = GetLastError();
|
||||
|
||||
@@ -702,7 +702,7 @@ bool GizmoObjectManipulation::reset_zero_button(ImGuiWrapper *imgui_wrapper, bo
|
||||
|
||||
for (int i = 0; i < number; i++)
|
||||
{
|
||||
char buf[3][64] = {0};
|
||||
char buf[3][64] = {};
|
||||
float buf_size[3] = {0};
|
||||
for (int j = 0; j < 3; j++) {
|
||||
ImGui::DataTypeFormatString(buf[j], IM_ARRAYSIZE(buf[j]), ImGuiDataType_Double, (void *) &vec[i][j], "%.2f");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -790,7 +790,7 @@ void IMSlider::draw_ticks(const ImRect& slideable_region) {
|
||||
|
||||
void IMSlider::show_tooltip(const std::string tooltip) {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 6 * m_scale, 3 * m_scale });
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, { 3 * m_scale });
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * m_scale);
|
||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, { 0,0,0,0 });
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
#include "Plater.hpp"
|
||||
#include "WebViewDialog.hpp"
|
||||
#include "../Utils/Process.hpp"
|
||||
#include "format.hpp"
|
||||
// BBS
|
||||
#include "PartPlate.hpp"
|
||||
#include "Preferences.hpp"
|
||||
@@ -49,11 +48,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"
|
||||
@@ -743,6 +740,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();
|
||||
});
|
||||
|
||||
@@ -1590,7 +1591,7 @@ void MainFrame::register_win32_callbacks()
|
||||
//static GUID GUID_DEVINTERFACE_USB_DEVICE = { 0xA5DCBF10, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED };
|
||||
//static GUID GUID_DEVINTERFACE_DISK = { 0x53f56307, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b };
|
||||
//static GUID GUID_DEVINTERFACE_VOLUME = { 0x71a27cdd, 0x812a, 0x11d0, 0xbe, 0xc7, 0x08, 0x00, 0x2b, 0xe2, 0x09, 0x2f };
|
||||
static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 };
|
||||
static GUID GUID_DEVINTERFACE_HID = { 0x4D1E55B2, 0xF16F, 0x11CF, { 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 } };
|
||||
|
||||
// Register USB HID (Human Interface Devices) notifications to trigger the 3DConnexion enumeration.
|
||||
DEV_BROADCAST_DEVICEINTERFACE NotificationFilter = { 0 };
|
||||
@@ -1630,7 +1631,7 @@ void MainFrame::register_win32_callbacks()
|
||||
|
||||
{
|
||||
static constexpr int device_count = 1;
|
||||
RAWINPUTDEVICE devices[device_count] = { 0 };
|
||||
RAWINPUTDEVICE devices[device_count] = {};
|
||||
// multi-axis mouse (SpaceNavigator, etc.)
|
||||
devices[0].usUsagePage = 0x01;
|
||||
devices[0].usUsage = 0x08;
|
||||
@@ -1739,6 +1740,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;
|
||||
@@ -2834,6 +2851,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();
|
||||
|
||||
@@ -4212,15 +4243,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)
|
||||
@@ -4229,6 +4268,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)
|
||||
@@ -4244,6 +4284,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;
|
||||
@@ -4264,6 +4305,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) {
|
||||
|
||||
@@ -178,6 +178,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);
|
||||
@@ -188,6 +189,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;
|
||||
};
|
||||
|
||||
@@ -341,6 +343,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
|
||||
@@ -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> ¬ification : 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> ¬ification : 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> ¬ification : 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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -66,7 +66,6 @@ class ParamsPanel : public wxPanel
|
||||
{
|
||||
#if __WXOSX__
|
||||
wxWindow* m_tmp_panel;
|
||||
int m_size_move = -1;
|
||||
#endif // __WXOSX__
|
||||
|
||||
private:
|
||||
|
||||
@@ -1112,7 +1112,7 @@ void PartPlate::show_tooltip(const std::string tooltip)
|
||||
{
|
||||
const auto scale = m_plater->get_current_canvas3D()->get_scale();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6 * scale, 3 * scale});
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, {3 * scale});
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * scale);
|
||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, {0, 0, 0, 0});
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
|
||||
|
||||
+399
-38
@@ -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();
|
||||
@@ -8272,7 +8273,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;
|
||||
@@ -8352,6 +8356,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) {
|
||||
@@ -8392,6 +8397,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;
|
||||
|
||||
@@ -8419,6 +8429,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)) {
|
||||
@@ -8582,7 +8693,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);
|
||||
@@ -8642,7 +8753,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);
|
||||
}
|
||||
@@ -8664,8 +8777,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;
|
||||
@@ -8721,6 +8836,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);
|
||||
@@ -8729,7 +8857,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;
|
||||
{
|
||||
@@ -8779,7 +8907,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;
|
||||
@@ -8824,12 +8952,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) {
|
||||
@@ -8970,7 +9142,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")) {
|
||||
@@ -9723,7 +9901,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) {
|
||||
@@ -9765,8 +9943,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:
|
||||
@@ -10004,6 +10185,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";
|
||||
|
||||
@@ -15104,14 +15290,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()) {
|
||||
@@ -15122,6 +15309,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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16779,22 +16975,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)
|
||||
@@ -18023,7 +18209,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);
|
||||
@@ -18032,6 +18217,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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -814,13 +814,13 @@ wxBoxSizer* SendMultiMachinePage::create_item_title(wxString title, wxWindow* pa
|
||||
wxBoxSizer* m_sizer_title = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
auto m_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0);
|
||||
m_title->SetForegroundColour(DESIGN_GRAY800_COLOR);
|
||||
m_title->SetForegroundColour(SEND_DESIGN_GRAY800_COLOR);
|
||||
m_title->SetFont(::Label::Head_13);
|
||||
m_title->Wrap(-1);
|
||||
m_title->SetToolTip(tooltip);
|
||||
|
||||
auto m_line = new wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(-1, 1), wxTAB_TRAVERSAL);
|
||||
m_line->SetBackgroundColour(DESIGN_GRAY400_COLOR);
|
||||
m_line->SetBackgroundColour(SEND_DESIGN_GRAY400_COLOR);
|
||||
|
||||
m_sizer_title->Add(m_title, 0, wxALIGN_CENTER | wxALL, 3);
|
||||
m_sizer_title->Add(0, 0, 0, wxLEFT, 9);
|
||||
@@ -843,7 +843,7 @@ wxBoxSizer* SendMultiMachinePage::create_item_checkbox(wxString title, wxWindow*
|
||||
m_sizer_checkbox->Add(0, 0, 0, wxEXPAND | wxLEFT, 8);
|
||||
|
||||
auto checkbox_title = new wxStaticText(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize, 0);
|
||||
checkbox_title->SetForegroundColour(DESIGN_GRAY900_COLOR);
|
||||
checkbox_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR);
|
||||
checkbox_title->SetFont(::Label::Body_13);
|
||||
|
||||
auto size = checkbox_title->GetTextExtent(title);
|
||||
@@ -867,12 +867,12 @@ wxBoxSizer* SendMultiMachinePage::create_item_input(wxString str_before, wxStrin
|
||||
{
|
||||
wxBoxSizer* sizer_input = new wxBoxSizer(wxHORIZONTAL);
|
||||
auto input_title = new wxStaticText(parent, wxID_ANY, str_before);
|
||||
input_title->SetForegroundColour(DESIGN_GRAY900_COLOR);
|
||||
input_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR);
|
||||
input_title->SetFont(::Label::Body_13);
|
||||
input_title->SetToolTip(tooltip);
|
||||
input_title->Wrap(-1);
|
||||
|
||||
auto input = new ::TextInput(parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER);
|
||||
auto input = new ::TextInput(parent, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, SEND_DESIGN_INPUT_SIZE, wxTE_PROCESS_ENTER);
|
||||
StateColor input_bg(std::pair<wxColour, int>(wxColour("#F0F0F1"), StateColor::Disabled), std::pair<wxColour, int>(*wxWHITE, StateColor::Enabled));
|
||||
input->SetBackgroundColor(input_bg);
|
||||
input->GetTextCtrl()->SetValue(app_config->get(param));
|
||||
@@ -880,7 +880,7 @@ wxBoxSizer* SendMultiMachinePage::create_item_input(wxString str_before, wxStrin
|
||||
input->GetTextCtrl()->SetValidator(validator);
|
||||
|
||||
auto second_title = new wxStaticText(parent, wxID_ANY, str_after, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
|
||||
second_title->SetForegroundColour(DESIGN_GRAY900_COLOR);
|
||||
second_title->SetForegroundColour(SEND_DESIGN_GRAY900_COLOR);
|
||||
second_title->SetFont(::Label::Body_13);
|
||||
second_title->SetToolTip(tooltip);
|
||||
second_title->Wrap(-1);
|
||||
@@ -1337,7 +1337,7 @@ wxPanel* SendMultiMachinePage::create_page()
|
||||
m_tip_text->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), -1));
|
||||
m_tip_text->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), -1));
|
||||
m_tip_text->SetLabel(_L("Please select the devices you would like to manage here (up to 6 devices)"));
|
||||
m_tip_text->SetForegroundColour(DESIGN_GRAY800_COLOR);
|
||||
m_tip_text->SetForegroundColour(SEND_DESIGN_GRAY800_COLOR);
|
||||
m_tip_text->SetFont(::Label::Head_20);
|
||||
m_tip_text->Wrap(-1);
|
||||
|
||||
|
||||
@@ -22,15 +22,15 @@ namespace GUI {
|
||||
#define SEND_LEFT_DEV_STATUS 250
|
||||
#define SEND_LEFT_TAKS_STATUS 180
|
||||
|
||||
#define DESIGN_SELECTOR_NOMORE_COLOR wxColour(248, 248, 248)
|
||||
#define DESIGN_GRAY900_COLOR wxColour(38, 46, 48)
|
||||
#define DESIGN_GRAY800_COLOR wxColour(50, 58, 61)
|
||||
#define DESIGN_GRAY600_COLOR wxColour(144, 144, 144)
|
||||
#define DESIGN_GRAY400_COLOR wxColour(166, 169, 170)
|
||||
#define DESIGN_RESOUTION_PREFERENCES wxSize(FromDIP(540), -1)
|
||||
#define DESIGN_COMBOBOX_SIZE wxSize(FromDIP(140), -1)
|
||||
#define DESIGN_LARGE_COMBOBOX_SIZE wxSize(FromDIP(160), -1)
|
||||
#define DESIGN_INPUT_SIZE wxSize(FromDIP(50), -1)
|
||||
#define SEND_DESIGN_SELECTOR_NOMORE_COLOR wxColour(248, 248, 248)
|
||||
#define SEND_DESIGN_GRAY900_COLOR wxColour(38, 46, 48)
|
||||
#define SEND_DESIGN_GRAY800_COLOR wxColour(50, 58, 61)
|
||||
#define SEND_DESIGN_GRAY600_COLOR wxColour(144, 144, 144)
|
||||
#define SEND_DESIGN_GRAY400_COLOR wxColour(166, 169, 170)
|
||||
#define SEND_DESIGN_RESOUTION_PREFERENCES wxSize(FromDIP(540), -1)
|
||||
#define SEND_DESIGN_COMBOBOX_SIZE wxSize(FromDIP(140), -1)
|
||||
#define SEND_DESIGN_LARGE_COMBOBOX_SIZE wxSize(FromDIP(160), -1)
|
||||
#define SEND_DESIGN_INPUT_SIZE wxSize(FromDIP(50), -1)
|
||||
|
||||
|
||||
|
||||
|
||||
+8
-17
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -162,6 +162,15 @@ 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;
|
||||
@@ -324,6 +333,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) {
|
||||
@@ -364,6 +380,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()
|
||||
@@ -388,6 +415,14 @@ void Button::messureSize()
|
||||
if (szIcon.y > szContent.y) 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());
|
||||
|
||||
@@ -4,28 +4,30 @@
|
||||
#include "../wxExtensions.hpp"
|
||||
#include "StaticBox.hpp"
|
||||
#include <wx/tipwin.h>
|
||||
#include <wx/colour.h>
|
||||
|
||||
class ButtonProps
|
||||
{
|
||||
public:
|
||||
static int ChoiceButtonGap(){return 10;};
|
||||
static int WindowButtonGap(){return 10;};
|
||||
static int ChoiceButtonGap() { return 10; };
|
||||
static int WindowButtonGap() { return 10; };
|
||||
};
|
||||
|
||||
enum class ButtonStyle{
|
||||
enum class ButtonStyle {
|
||||
Regular,
|
||||
Confirm,
|
||||
Alert,
|
||||
Disabled,
|
||||
};
|
||||
|
||||
enum class ButtonType{
|
||||
Compact , // Font10 FullyRounded For spaces with less areas
|
||||
Window , // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes
|
||||
Choice , // Font14 Semi-Rounded For dialog/window choice buttons
|
||||
enum class ButtonType {
|
||||
Compact, // Font10 FullyRounded For spaces with less areas
|
||||
Window, // Font12 FullyRounded For regular buttons in windows and not related with parameter boxes
|
||||
Choice, // Font14 Semi-Rounded For dialog/window choice buttons
|
||||
Parameter, // Font14 Semi-Rounded For buttons that near parameter boxes
|
||||
Icon , // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while creation of button
|
||||
Expanded , // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
|
||||
Icon, // ------ Semi-Rounded For buttons that only has icons. icons should be 16x16 and iconSize has to be defined as 16 while
|
||||
// creation of button
|
||||
Expanded, // Font14 Semi-Rounded For full length buttons. ex. buttons in static box
|
||||
};
|
||||
|
||||
class Button : public StaticBox
|
||||
@@ -36,15 +38,17 @@ class Button : public StaticBox
|
||||
wxSize paddingSize;
|
||||
ScalableBitmap active_icon;
|
||||
|
||||
StateColor text_color;
|
||||
StateColor text_color;
|
||||
|
||||
bool pressedDown = false;
|
||||
bool m_selected = true;
|
||||
bool canFocus = true;
|
||||
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 buttonWidth = 200;
|
||||
static const int buttonHeight = 50;
|
||||
|
||||
public:
|
||||
@@ -68,19 +72,23 @@ public:
|
||||
|
||||
void SetStyle(const ButtonStyle style /*= ButtonStyle::Regular*/, const ButtonType type /*= ButtonType::None*/);
|
||||
|
||||
void SetTextColor(StateColor const &color);
|
||||
void SetTextColor(StateColor const& color);
|
||||
|
||||
void SetTextColorNormal(wxColor const &color);
|
||||
void SetTextColorNormal(wxColor const& color);
|
||||
|
||||
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; }
|
||||
bool IsSelected() const { return m_selected; }
|
||||
ButtonType GetType() const { return m_type; }
|
||||
bool IsSelected() const { return m_selected; }
|
||||
|
||||
bool Enable(bool enable = true) override;
|
||||
void EnableTooltipEvenDisabled();// The tip will be shown even if the button is disabled
|
||||
void EnableTooltipEvenDisabled(); // The tip will be shown even if the button is disabled
|
||||
|
||||
void SetCanFocus(bool canFocus) override;
|
||||
|
||||
@@ -104,7 +112,7 @@ protected:
|
||||
private:
|
||||
bool m_has_style = false;
|
||||
ButtonStyle m_style;
|
||||
ButtonType m_type;
|
||||
ButtonType m_type;
|
||||
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
|
||||
@@ -115,10 +123,10 @@ private:
|
||||
// some useful events
|
||||
void mouseDown(wxMouseEvent& event);
|
||||
void mouseReleased(wxMouseEvent& event);
|
||||
void mouseCaptureLost(wxMouseCaptureLostEvent &event);
|
||||
void keyDownUp(wxKeyEvent &event);
|
||||
void mouseCaptureLost(wxMouseCaptureLostEvent& event);
|
||||
void keyDownUp(wxKeyEvent& event);
|
||||
|
||||
//
|
||||
//
|
||||
void sendButtonEvent();
|
||||
|
||||
// parent motion
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,6 +46,8 @@ public:
|
||||
protected:
|
||||
void eraseEvent(wxEraseEvent& evt);
|
||||
|
||||
void sizeEvent(wxSizeEvent& evt);
|
||||
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
|
||||
void render(wxDC& dc);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -57,7 +57,7 @@ void set_miniaturizable(void * window)
|
||||
while(viewObject = (NSView *)[viewEnum nextObject]) {
|
||||
if([viewObject class] == [NSTextField self]) {
|
||||
//[(NSTextField*)viewObject setTextColor : NSColor.whiteColor];
|
||||
mainframe_text_field = viewObject;
|
||||
mainframe_text_field = (NSTextField*)viewObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ void set_title_colour_after_set_title(void * window)
|
||||
while(viewObject = (NSView *)[viewEnum nextObject]) {
|
||||
if([viewObject class] == [NSTextField self]) {
|
||||
[(NSTextField*)viewObject setTextColor : NSColor.whiteColor];
|
||||
mainframe_text_field = viewObject;
|
||||
mainframe_text_field = (NSTextField*)viewObject;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1051,8 +1106,8 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
|
||||
|
||||
const auto is_vendor_enabled = (vendor_name == PresetBundle::ORCA_DEFAULT_BUNDLE) // always update configs from resource to vendor for ORCA_DEFAULT_BUNDLE
|
||||
|| (enabled_vendors.find(vendor_name) != enabled_vendors.end());
|
||||
if (enabled_config_update) {
|
||||
if (is_vendor_installed(vendor_name)) {
|
||||
if (is_vendor_installed(vendor_name)) {
|
||||
if (enabled_config_update) {
|
||||
if (is_vendor_enabled) {
|
||||
// Orca: whichever form of the vendor resources ships at the newer
|
||||
// version is the one installing lays down, and the one to judge
|
||||
@@ -1067,17 +1122,12 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
|
||||
<< resource_ver.to_string() << " from resource, old version " << vendor_ver.to_string();
|
||||
bundles.insert(vendor_name);
|
||||
}
|
||||
}
|
||||
else {
|
||||
//need to be removed because not installed
|
||||
} else {
|
||||
// need to be removed because not installed
|
||||
remove_installed_vendor(vendor_name);
|
||||
}
|
||||
}
|
||||
else if (is_vendor_enabled) {
|
||||
bundles.insert(vendor_name);
|
||||
}
|
||||
}
|
||||
else if (is_vendor_enabled) {
|
||||
} else if (is_vendor_enabled) {
|
||||
bundles.insert(vendor_name);
|
||||
}
|
||||
}
|
||||
@@ -1147,68 +1197,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 +1409,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 +1444,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 +1761,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; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user