Merge branch 'main' into feat/generic-n-extruders

This commit is contained in:
Ian Chua committed 2026-10-15 16:04:30 +08:00
commit 46096416cd
473 files changed
+127462 -21603

No files matched your search

+16 -1
View File
@@ -144,6 +144,7 @@
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Exception.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/GCode/GCodeStatistics.hpp"
#include "libslic3r/GCode/ToolOrdering.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -207,6 +208,7 @@ std::map<int, std::string> cli_errors = {
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
{CLI_EXPORT_STATISTICS_ERROR, "Failed exporting the G-code statistics."},
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
@@ -1539,6 +1541,9 @@ int CLI::run(int argc, char **argv)
if (allow_mix_temp_option)
allow_mix_temp = allow_mix_temp_option->value;
const ConfigOptionBool* export_statistics_option = m_config.option<ConfigOptionBool>("export_statistics");
const bool export_statistics = export_statistics_option != nullptr && export_statistics_option->value;
ConfigOptionBool* avoid_extrusion_cali_region_option = m_config.option<ConfigOptionBool>("avoid_extrusion_cali_region");
if (avoid_extrusion_cali_region_option)
avoid_extrusion_cali_region = avoid_extrusion_cali_region_option->value;
@@ -7081,6 +7086,16 @@ int CLI::run(int argc, char **argv)
record_exit_reson(outfile_dir, CLI_GCODE_PATH_IN_UNPRINTABLE_AREA, index + 1, cli_errors[CLI_GCODE_PATH_IN_UNPRINTABLE_AREA], sliced_info);
flush_and_exit(CLI_GCODE_PATH_IN_UNPRINTABLE_AREA);
}
// Without an output directory the G-code is a temporary file, kept only in an exported 3MF.
if (export_statistics && !outfile_dir.empty()) {
try {
export_gcode_statistics(*gcode_result, gcode_statistics_path(outfile));
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "plate " << index + 1 << ": " << ex.what();
record_exit_reson(outfile_dir, CLI_EXPORT_STATISTICS_ERROR, index + 1, cli_errors[CLI_EXPORT_STATISTICS_ERROR], sliced_info);
flush_and_exit(CLI_EXPORT_STATISTICS_ERROR);
}
}
//outfile_final = (dynamic_cast<Print*>(print))->print_statistics().finalize_output_path(outfile);
@@ -7209,7 +7224,7 @@ int CLI::run(int argc, char **argv)
std::vector<ThumbnailData *> thumbnails, no_light_thumbnails, top_thumbnails, pick_thumbnails;
std::vector<PlateBBoxData*> plate_bboxes;
PlateDataPtrs plate_data_list;
partplate_list.store_to_3mf_structure(plate_data_list);
partplate_list.store_to_3mf_structure(plate_data_list, true, -1, export_statistics);
if (sliced_plate == -1) {
for (int i = 0; i < plate_data_list.size(); i++) {
+23
View File
@@ -399,6 +399,9 @@ void AppConfig::set_defaults()
if (get("show_plate_gridlines").empty())
set_bool("show_plate_gridlines", true);
if (get("focus_mode").empty())
set_bool("focus_mode", false);
if (get("show_outline").empty())
set_bool("show_outline", false);
@@ -1374,6 +1377,26 @@ bool AppConfig::write_config_file(const std::string &path, std::string body, con
return true;
}
void AppConfig::clear_local_machine_access_code(const std::string& dev_id, const std::string& agent_id)
{
// Unscoped credentials predate printer agents and belong to the legacy BBL agent.
const bool is_bbl_agent = agent_id.empty() || agent_id == "bbl";
auto it = m_local_machines.find(dev_id);
if (it != m_local_machines.end()) {
const std::string& stored_agent = it->second.printer_agent_id;
if (stored_agent == agent_id || (is_bbl_agent && (stored_agent.empty() || stored_agent == "bbl"))) {
if (!it->second.access_code.empty()) {
it->second.access_code.clear();
m_dirty = true;
}
}
}
if (is_bbl_agent) {
erase("access_code", dev_id);
erase("user_access_code", dev_id);
}
}
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
{
const auto it_v = m_vendors.find(vendor);
+2
View File
@@ -306,6 +306,8 @@ public:
}
}
void clear_local_machine_access_code(const std::string& dev_id, const std::string& agent_id);
const std::vector<std::string> &get_filament_presets() const { return m_filament_presets; }
void set_filament_presets(const std::vector<std::string> &filament_presets){
m_filament_presets = filament_presets;
+1 -3
View File
@@ -300,12 +300,10 @@ template<class PConf>
void fill_config(PConf& pcfg, const ArrangeParams &params) {
if (params.is_belt) {
// Pack from the end of the belt that prints first, and keep the pile on the
// bed when it is larger than the room around that end.
// Pack from the end of the belt that prints first.
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
PConf::Alignment::BOTTOM_RIGHT;
pcfg.clamp_to_bin = true;
}
else if (params.is_seq_print) {
// Start placing the items from the center of the print bed
+1 -1
View File
@@ -166,7 +166,7 @@ void Print::_plan_belt_purge()
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
throw Slic3r::SlicingError(_u8L("The print is empty. The model is not printable with current print settings."));
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
+26 -23
View File
@@ -1807,7 +1807,8 @@ std::vector<std::pair<std::string, SketchPlane>> CadDocument::resolve_datum_plan
}
out.emplace_back(base_missing ? f.name + " (base plane missing)" : f.name, result);
// TRN Datum plane entry in the reference plane list. %1% is the plane name.
out.emplace_back(base_missing ? format(_u8L("%1% (base plane missing)"), f.name) : f.name, result);
}
return out;
}
@@ -2090,8 +2091,8 @@ static int pick_body(int ref, int nb)
{
if (ref == -1) return nb - 1;
if (ref < 0 || ref >= nb)
throw std::runtime_error("a feature works on body " + std::to_string(ref + 1)
+ ", which does not exist at that point in the history");
throw std::runtime_error(format(_u8L("a feature works on body %1%, which does not exist at that point in the history"),
ref + 1));
return ref;
}
@@ -2154,11 +2155,11 @@ static void settle_body_refs(CadFeature& f, const std::vector<CadBody>& v, bool
CadFeature::BodyId& id = f.body_ref_ids[k++];
if (kind != running) return;
if (ref == kBodyGone)
throw std::runtime_error("\"" + f.name + "\" works on a body made by a feature that was deleted or hidden");
throw std::runtime_error(format(_u8L("\"%1%\" works on a body made by a feature that was deleted or hidden"), f.name));
if (f.body_refs_pending && id.src >= 0) {
const int i = find_body(v, id);
if (i < 0)
throw std::runtime_error("\"" + f.name + "\" works on a body that does not exist at that point in the history");
throw std::runtime_error(format(_u8L("\"%1%\" works on a body that does not exist at that point in the history"), f.name));
ref = i;
}
id = ref >= 0 ? body_id_of(v, ref) : CadFeature::BodyId{};
@@ -2384,7 +2385,8 @@ bool CadDocument::move_feature(int index, int delta)
check(f.sketch_ref); check(f.sweep_path_ref); check(f.pattern_curve_sketch); check(f.rib_sketch_ref);
for (int& r : f.loft_profile_refs) check(r);
if (!bad.empty()) {
error = "\"" + f.name + "\" uses \"" + bad + "\", so it cannot come before it";
// TRN Feature reorder error. %1% is the moved feature, %2% the feature it depends on.
error = format(_u8L("\"%1%\" uses \"%2%\", so it cannot come before it"), f.name, bad);
features.swap(snapshot);
return false;
}
@@ -2463,7 +2465,8 @@ static std::string open_loop_message(const CadFeature& sketch,
for (size_t i = 0; i < shown; ++i)
msg += "\n " + format(_u8L("Open at (%.3f, %.3f)"), open[i].x(), open[i].y());
if (open.size() > shown)
msg += "\n " + format(_u8L("...and %1% more open end(s)"), open.size() - shown);
// TRN Follows a list of open sketch ends. %1% is how many more were not listed.
msg += "\n " + format(_u8L("Open ends not shown: %1%"), open.size() - shown);
}
return msg;
}
@@ -2475,11 +2478,11 @@ static gp_Ax1 revolve_axis_of(const CadFeature& f, const CadFeature& sk)
if (f.revolve_axis_entity >= 0) {
if (f.revolve_axis_entity >= int(sk.entities.size())
|| sk.entities[f.revolve_axis_entity].type != SketchEntity::Type::Line)
throw std::runtime_error("revolve: the axis line is no longer in the sketch — pick the axis again");
throw std::runtime_error(_u8L("revolve: the axis line is no longer in the sketch — pick the axis again"));
const SketchEntity& e = sk.entities[f.revolve_axis_entity];
const Vec3d a = sk.plane.to_world(e.p0), b = sk.plane.to_world(e.p1);
if ((b - a).norm() < 1e-9)
throw std::runtime_error("revolve: the axis line has no length");
throw std::runtime_error(_u8L("revolve: the axis line has no length"));
const Vec3d d = (b - a).normalized();
return gp_Ax1(gp_Pnt(a.x(), a.y(), a.z()), gp_Dir(d.x(), d.y(), d.z()));
}
@@ -3024,17 +3027,17 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
// ISO external thread depth is ~0.61*P, so allow up to 0.7*P (0.49 wrongly rejected
// every real thread -> nothing rendered). Still bound it well under a full pitch.
// R >= 0.5 (not >): M1, the smallest size in the standards table, is exactly 0.5.
const char* why = R < 0.5 ? "thread: the diameter must be at least 1 mm"
: P <= 0.1 ? "thread: the pitch must be more than 0.1 mm"
: (D <= 1e-3 || D >= 0.7 * P) ? "thread: the depth must be between 0 and 0.7 x the pitch"
: D >= 0.45 * R ? "thread: the depth is too large for this diameter"
: H <= 0.5 * P ? "thread: the length must be more than half a pitch"
: H / P >= 400.0 ? "thread: too many turns (length / pitch must stay under 400)"
const char* why = R < 0.5 ? L("thread: the diameter must be at least 1 mm")
: P <= 0.1 ? L("thread: the pitch must be more than 0.1 mm")
: (D <= 1e-3 || D >= 0.7 * P) ? L("thread: the depth must be between 0 and 0.7 x the pitch")
: D >= 0.45 * R ? L("thread: the depth is too large for this diameter")
: H <= 0.5 * P ? L("thread: the length must be more than half a pitch")
: H / P >= 400.0 ? L("thread: too many turns (length / pitch must stay under 400)")
: nullptr;
// Legacy recipes keep their old outcome (the body left as it was) so a project that
// opened before still opens; every thread made now reports the reason instead.
if (why != nullptr) {
if (f.thread_major_nominal) throw std::runtime_error(why);
if (f.thread_major_nominal) throw std::runtime_error(Slic3r::I18N::translate(why));
break;
}
}
@@ -3078,7 +3081,7 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body,
// Tapped hole: bore to the minor radius R - D, then cut the groove out to the
// major radius R. The bore sits 10 um inside the minor radius so that, threading
// an existing tap-drill hole, it does not coincide with that hole's wall.
if (!have_body) throw std::runtime_error("internal thread needs a body");
if (!have_body) throw std::runtime_error(_u8L("internal thread needs a body"));
const double bore_r = R - D - 0.01;
cut_from(result, BRepPrimAPI_MakeCylinder(ax2, bore_r, f.thread_height).Shape());
TopoDS_Shape groove;
@@ -3241,12 +3244,12 @@ void CadDocument::apply_boolean(std::vector<CadBody>& bodies, const CadFeature&
const int tool = f.bool_tool_body == -1 ? -1 : pick_body(f.bool_tool_body, nb);
// Same rule as Cut, Mirror and Transform: an operation that cannot run says why, rather
// than leaving the bodies as they were and reporting success.
if (tgt < 0) throw std::runtime_error("boolean: no target body");
if (tool < 0) throw std::runtime_error("boolean: no tool body — pick a second body");
if (tgt == tool) throw std::runtime_error("boolean: the target and the tool are the same body");
if (tgt < 0) throw std::runtime_error(_u8L("boolean: no target body"));
if (tool < 0) throw std::runtime_error(_u8L("boolean: no tool body — pick a second body"));
if (tgt == tool) throw std::runtime_error(_u8L("boolean: the target and the tool are the same body"));
const TopoDS_Shape A = bodies[tgt].shape; // target survives
const TopoDS_Shape B = bodies[tool].shape; // tool, consumed unless kept
if (A.IsNull() || B.IsNull()) throw std::runtime_error("boolean: a body has no geometry");
if (A.IsNull() || B.IsNull()) throw std::runtime_error(_u8L("boolean: a body has no geometry"));
NCollection_List<TopoDS_Shape> args, tools;
args.Append(A);
@@ -3554,7 +3557,7 @@ void CadDocument::apply_thicken_surface(std::vector<CadBody>& bodies, const CadF
if (props.Mass() < 0.0) solid.Reverse();
}
bodies.push_back({solid, f.name.empty() ? std::string("ThickenSurface") : f.name});
bodies.push_back({solid, f.name.empty() ? _u8L("Thicken Surface") : f.name});
}
void CadDocument::apply_surface_offset(std::vector<CadBody>& bodies, const CadFeature& f) const
@@ -3573,7 +3576,7 @@ void CadDocument::apply_surface_offset(std::vector<CadBody>& bodies, const CadFe
TopoDS_Shape off_shape = mos.Shape();
if (off_shape.IsNull()) throw std::runtime_error(_u8L("surface-offset: produced no geometry"));
bodies.push_back({off_shape, f.name.empty() ? std::string("SurfaceOffset") : f.name});
bodies.push_back({off_shape, f.name.empty() ? _u8L("Surface Offset") : f.name});
}
// Project `edges` of a shape onto `plane`, appending the resulting 2D entities to `out`.
+6 -6
View File
@@ -325,10 +325,10 @@ std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid
// handing the solid back unchanged, which recompute then reported as a success.
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces)
{
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
if (radius <= 0.001) throw std::runtime_error(_u8L("the fillet radius must be greater than 0"));
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) throw std::runtime_error("no edges to fillet in that group");
if (edges.empty()) throw std::runtime_error(_u8L("no edges to fillet in that group"));
BRepFilletAPI_MakeFillet fillet(solid);
for (const auto& edge : edges)
@@ -344,10 +344,10 @@ TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radi
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, FaceGroup faces)
{
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
if (distance <= 0.001) throw std::runtime_error(_u8L("the chamfer distance must be greater than 0"));
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) throw std::runtime_error("no edges to chamfer in that group");
if (edges.empty()) throw std::runtime_error(_u8L("no edges to chamfer in that group"));
BRepFilletAPI_MakeChamfer chamfer(solid);
for (const auto& edge : edges)
@@ -370,7 +370,7 @@ TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double dis
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, const std::vector<int>& edge_ids)
{
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
if (radius <= 0.001) throw std::runtime_error(_u8L("the fillet radius must be greater than 0"));
if (edge_ids.empty()) throw std::runtime_error("apply_fillet: no edge picked");
BRepFilletAPI_MakeFillet mk(solid);
@@ -387,7 +387,7 @@ TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radi
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, const std::vector<int>& edge_ids)
{
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
if (distance <= 0.001) throw std::runtime_error(_u8L("the chamfer distance must be greater than 0"));
if (edge_ids.empty()) throw std::runtime_error("apply_chamfer: no edge picked");
BRepFilletAPI_MakeChamfer mk(solid);
+3 -3
View File
@@ -379,7 +379,7 @@ TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const gp_Ax1& a
}
}
if (pos && neg)
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
throw std::runtime_error(_u8L("the profile crosses the revolve axis — it must lie on one side of it"));
}
gp_Ax1 axis = axis_in;
double angle_rad = angle_deg * M_PI / 180.0;
@@ -390,7 +390,7 @@ TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const gp_Ax1& a
if (!rev.IsDone())
throw std::runtime_error(_u8L("Failed to revolve"));
if (!BRepCheck_Analyzer(rev.Shape()).IsValid())
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
throw std::runtime_error(_u8L("the profile crosses the revolve axis — it must lie on one side of it"));
return rev.Shape();
}
@@ -923,7 +923,7 @@ static TopoDS_Face checked_profile_face(const TopoDS_Face& f)
{
BRepCheck_Analyzer an(f);
if (!an.IsValid())
throw std::runtime_error("the profile crosses or folds back on itself, so it does not bound one region");
throw std::runtime_error(_u8L("the profile crosses or folds back on itself, so it does not bound one region"));
return f;
}
+2
View File
@@ -265,6 +265,8 @@ set(lisbslic3r_sources
GCode/FanMover.hpp
GCode/GCodeProcessor.cpp
GCode/GCodeProcessor.hpp
GCode/GCodeStatistics.cpp
GCode/GCodeStatistics.hpp
GCode/ElegooGCodeProcessorHelper.cpp
GCode.hpp
GCode/PchipInterpolatorHelper.cpp
+9 -8
View File
@@ -2,6 +2,7 @@
#include "Exception.hpp"
#include "Point.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include "Utils.hpp"
#include "LocalesUtils.hpp"
#include "Preset.hpp"
@@ -613,7 +614,7 @@ bool ConfigBase::set_deserialize_nothrow(const t_config_option_key &opt_key_src,
void ConfigBase::set_deserialize(const t_config_option_key &opt_key_src, const std::string &value_src, ConfigSubstitutionContext& substitutions_ctxt, bool append)
{
if (! this->set_deserialize_nothrow(opt_key_src, value_src, substitutions_ctxt, append))
throw BadOptionValueException(format("Invalid value provided for parameter %1%: %2%", opt_key_src, value_src));
throw BadOptionValueException(format(_u8L("Invalid value provided for parameter %1%: %2%"), opt_key_src, value_src));
}
void ConfigBase::set_deserialize(std::initializer_list<SetDeserializeItem> items, ConfigSubstitutionContext& substitutions_ctxt)
@@ -1173,7 +1174,7 @@ ConfigSubstitutions ConfigBase::load_from_ini(const std::string &file, ForwardCo
boost::property_tree::read_ini(ifs, tree);
return this->load(tree, compatibility_rule);
} catch (const ConfigurationError &e) {
throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file, e.what()));
throw ConfigurationError(format(_u8L("Failed loading configuration file \"%1%\": %2%"), file, e.what()));
}
}
@@ -1430,9 +1431,8 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
}
}
if (!header_found) {
std::string error_message = "Not a gcode file generated by ";
error_message += SLIC3R_APP_FULL_NAME;
error_message += ".";
// TRN %1% is the application name.
std::string error_message = format(_u8L("Not a gcode file generated by %1%."), SLIC3R_APP_FULL_NAME);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << error_message;
throw Slic3r::RuntimeError(error_message.c_str());
}
@@ -1458,7 +1458,8 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
}
if (!begin_found) {
//BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << format("Configuration block closing tag \"; CONFIG_BLOCK_START\" not found when reading %1%", file);
throw Slic3r::RuntimeError(format("Config tag \"; CONFIG_BLOCK_START\" not found"));
// TRN %1% is a tag in the G-code file that must not be translated.
throw Slic3r::RuntimeError(format(_u8L("Config tag \"%1%\" not found"), "; CONFIG_BLOCK_START"));
}
std::string key, value;
while (std::getline(ifs, line)) {
@@ -1483,7 +1484,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
}
if (!end_found) {
//BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << format("Configuration block opening tag \"; CONFIG_BLOCK_END\" not found when reading %1%", file);
throw Slic3r::RuntimeError(format("Config tag \"; CONFIG_BLOCK_END\" not found"));
throw Slic3r::RuntimeError(format(_u8L("Config tag \"%1%\" not found"), "; CONFIG_BLOCK_END"));
}
}
else
@@ -1503,7 +1504,7 @@ ConfigSubstitutions ConfigBase::load_from_gcode_file(const std::string &file, Fo
if (key_value_pairs < 80) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs);
throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", file, key_value_pairs));
throw Slic3r::RuntimeError(format(_u8L("Suspiciously low number of configuration values extracted from %1%: %2%"), file, key_value_pairs));
}
// Do legacy conversion on a completely loaded dictionary.
+3 -2
View File
@@ -7,6 +7,7 @@
#include <algorithm>
#include <cmath>
#include "Config.hpp"
#include "I18N.hpp"
namespace Slic3r {
@@ -124,9 +125,9 @@ double Extruder::unretract()
void Extruder::set_retracted(double retracted, double restart_extra)
{
if (retracted < - EPSILON)
throw Slic3r::RuntimeError("Custom G-code reports negative z_retracted.");
throw Slic3r::RuntimeError(_u8L("Custom G-code reports negative z_retracted."));
if (restart_extra < - EPSILON)
throw Slic3r::RuntimeError("Custom G-code reports negative z_restart_extra.");
throw Slic3r::RuntimeError(_u8L("Custom G-code reports negative z_restart_extra."));
if (retracted > EPSILON) {
m_retracted = retracted;
+28 -28
View File
@@ -19,6 +19,8 @@
#include "ColorDecomposeRecipe.hpp"
#include "Config.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "FilamentMixerModel.hpp"
#include "LocalesUtils.hpp"
@@ -633,36 +635,36 @@ bool try_parse_mixed_components_strict(const std::string &str,
{
components.clear();
if (str.empty()) {
err = "empty component list";
err = _u8L("empty component list");
return false;
}
std::istringstream ss(str);
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty component index";
err = _u8L("empty component index");
return false;
}
try {
const long val = std::stol(token);
if (val < 1) {
err = "component index must be >= 1 (got " + token + ")";
err = format(_u8L("component index must be >= 1 (got %1%)"), token);
return false;
}
components.push_back(static_cast<unsigned int>(val));
} catch (...) {
err = "invalid component index \"" + token + "\"";
err = format(_u8L("invalid component index \"%1%\""), token);
return false;
}
}
if (components.size() < 2) {
err = "at least 2 components required (got " + std::to_string(components.size()) + ")";
err = format(_u8L("at least 2 components required (got %1%)"), components.size());
return false;
}
std::set<unsigned int> seen;
for (unsigned int c : components) {
if (!seen.insert(c).second) {
err = "duplicate component index " + std::to_string(c);
err = format(_u8L("duplicate component index %1%"), c);
return false;
}
}
@@ -682,24 +684,24 @@ bool try_parse_mixed_ratios_strict(const std::string &str,
std::string token;
while (std::getline(ss, token, ',')) {
if (token.empty()) {
err = "empty ratio value";
err = _u8L("empty ratio value");
return false;
}
try {
const double val = std::stod(token);
if (!(val > 0.0)) {
err = "ratio must be positive (got " + token + ")";
err = format(_u8L("ratio must be positive (got %1%)"), token);
return false;
}
ratios.push_back(val);
} catch (...) {
err = "invalid ratio \"" + token + "\"";
err = format(_u8L("invalid ratio \"%1%\""), token);
return false;
}
}
if (ratios.size() != n_components) {
err = "expected " + std::to_string(n_components) + " ratio(s), got "
+ std::to_string(ratios.size());
// TRN %1% is the expected number of ratios, %2% the number found.
err = format(_u8L("the ratio count must match the component count: expected %1%, got %2%"), n_components, ratios.size());
return false;
}
return true;
@@ -713,11 +715,11 @@ bool validate_gradient_range_strict(const std::string &str, std::string &err)
CNumericLocalesSetter c_locale_setter;
float v0 = 0.f, v1 = 0.f;
if (std::sscanf(str.c_str(), "%f,%f", &v0, &v1) != 2) {
err = "expected two comma-separated floats, e.g. \"0.10,0.90\"";
err = _u8L("expected two comma-separated floats, e.g. \"0.10,0.90\"");
return false;
}
if (!(v0 > 0.f && v0 < 1.f && v1 > 0.f && v1 < 1.f)) {
err = "start and end ratios must be in (0, 1)";
err = _u8L("start and end ratios must be in (0, 1)");
return false;
}
return true;
@@ -771,8 +773,7 @@ static void check_mixed_array_size_required(std::map<std::string, std::string> &
{
if (actual_size != expected_size) {
append_error(errors, opt_key,
"array size " + std::to_string(actual_size)
+ " does not match filament slot count " + std::to_string(expected_size));
format(_u8L("array size %1% does not match filament slot count %2%"), actual_size, expected_size));
}
}
@@ -789,7 +790,7 @@ std::map<std::string, std::string> validate_mixed_filament_params(
if (has_mixed_sub_params_specified(comp_strs, ratio_strs, gradient_flags)
&& !has_any_mixed_filament(is_mixed)) {
append_error(errors, "filament_is_mixed",
"must be set when mixed filament parameters are specified");
_u8L("must be set when mixed filament parameters are specified"));
return errors;
}
@@ -822,34 +823,33 @@ std::map<std::string, std::string> validate_mixed_filament_params(
if (!is_mixed[i])
continue;
const std::string slot = "slot " + std::to_string(i + 1);
const size_t slot = i + 1;
const std::string comp_str = i < comp_strs.size() ? comp_strs[i] : "";
std::vector<unsigned int> components;
std::string comp_err;
if (!try_parse_mixed_components_strict(comp_str, components, comp_err)) {
append_error(errors, "filament_mixed_components", slot + ": " + comp_err);
// TRN Mixed filament validation error. %1% is the filament slot number, %2% the reason.
append_error(errors, "filament_mixed_components", format(_u8L("slot %1%: %2%"), slot, comp_err));
continue;
}
for (unsigned int c : components) {
if (c > num_physical) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " out of range (max physical filament index is "
+ std::to_string(num_physical) + ")");
format(_u8L("slot %1%: component %2% out of range (max physical filament index is %3%)"),
slot, c, num_physical));
break;
}
if (c == i + 1) {
append_error(errors, "filament_mixed_components",
slot + ": cannot reference itself as a component");
format(_u8L("slot %1%: cannot reference itself as a component"), slot));
break;
}
const size_t idx0 = static_cast<size_t>(c - 1);
if (idx0 < is_mixed.size() && is_mixed[idx0]) {
append_error(errors, "filament_mixed_components",
slot + ": component " + std::to_string(c)
+ " references a mixed filament slot");
format(_u8L("slot %1%: component %2% references a mixed filament slot"), slot, c));
break;
}
}
@@ -857,27 +857,27 @@ std::map<std::string, std::string> validate_mixed_filament_params(
std::string ratio_err;
const std::string ratio_str = i < ratio_strs.size() ? ratio_strs[i] : "";
if (!try_parse_mixed_ratios_strict(ratio_str, components.size(), ratio_err))
append_error(errors, "filament_mixed_sublayer_ratios", slot + ": " + ratio_err);
append_error(errors, "filament_mixed_sublayer_ratios", format(_u8L("slot %1%: %2%"), slot, ratio_err));
const bool gradient_on = i < gradient_flags.size() && gradient_flags[i];
if (gradient_on) {
if (components.size() != 2) {
append_error(errors, "filament_mixed_gradient",
slot + ": gradient requires exactly 2 components");
format(_u8L("slot %1%: gradient requires exactly 2 components"), slot));
}
if (gradient_specified) {
std::string range_err;
const std::string range_str = i < gradient_range_strs.size() ? gradient_range_strs[i] : "";
if (!validate_gradient_range_strict(range_str, range_err))
append_error(errors, "filament_mixed_gradient_range", slot + ": " + range_err);
append_error(errors, "filament_mixed_gradient_range", format(_u8L("slot %1%: %2%"), slot, range_err));
}
if (curve_specified) {
const std::string curve_str = i < gradient_curve_strs.size() ? gradient_curve_strs[i] : "";
if (!curve_str.empty() && parse_gradient_curve(curve_str).empty())
append_error(errors, "filament_mixed_gradient_curve",
slot + ": invalid curve (need at least 2 valid control points)");
format(_u8L("slot %1%: invalid curve (need at least 2 valid control points)"), slot));
}
}
}
+6 -4
View File
@@ -10,6 +10,7 @@
#include <string>
#include <math.h>
#include "PrintConfig.hpp"
#include "I18N.hpp"
// Mark string for localization and translate.
#define L(s) Slic3r::I18N::translate(s)
@@ -17,10 +18,10 @@
namespace Slic3r {
FlowErrorNegativeSpacing::FlowErrorNegativeSpacing() :
FlowError("Flow::spacing() produced negative spacing. Did you set some extrusion width too small?") {}
FlowError(L("Flow::spacing() produced negative spacing. Did you set some extrusion width too small?")) {}
FlowErrorNegativeFlow::FlowErrorNegativeFlow() :
FlowError("Flow::mm3_per_mm() produced negative flow. Did you set some extrusion width too small?") {}
FlowError(L("Flow::mm3_per_mm() produced negative flow. Did you set some extrusion width too small?")) {}
// This static method returns a sane extrusion width default.
float Flow::auto_extrusion_width(FlowRole role, float nozzle_diameter)
@@ -66,7 +67,8 @@ static inline FlowRole opt_key_to_flow_role(const std::string &opt_key)
static inline void throw_on_missing_variable(const std::string &opt_key, const char *dependent_opt_key)
{
throw FlowErrorMissingVariable((boost::format("Failed to calculate line width of %1%. Cannot get value of \u201c%2%\u201d.") % opt_key % dependent_opt_key).str());
// TRN %1% and %2% are names of settings.
throw FlowErrorMissingVariable((boost::format(L("Failed to calculate line width of %1%. Cannot get value of \"%2%\".")) % opt_key % dependent_opt_key).str());
}
// Used to provide hints to the user on default extrusion width values, and to provide reasonable values to the PlaceholderParser.
@@ -162,7 +164,7 @@ Flow Flow::with_spacing(float new_spacing) const
assert(m_width >= m_height);
out.m_width += new_spacing - m_spacing;
if (out.m_width < out.m_height)
throw Slic3r::InvalidArgument("Invalid spacing supplied to Flow::with_spacing(), check your layer height and extrusion width.");
throw Slic3r::InvalidArgument(L("Invalid spacing supplied to Flow::with_spacing(), check your layer height and extrusion width."));
}
out.m_spacing = new_spacing;
return out;
+3 -2
View File
@@ -4,6 +4,7 @@
#include "libslic3r/Utils.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/I18N.hpp"
#include "STEP.hpp"
@@ -68,7 +69,7 @@ bool StepPreProcessor::preprocess(const char* path, std::string &output_path)
{
boost::nowide::ifstream infile(path);
if (!infile.good()) {
throw Slic3r::RuntimeError(std::string("Load step file failed.\nCannot open file for reading.\n"));
throw Slic3r::RuntimeError(_u8L("Load step file failed.\nCannot open file for reading.\n"));
return false;
}
@@ -116,7 +117,7 @@ bool StepPreProcessor::isUtf8File(const char* path)
{
boost::nowide::ifstream infile(path);
if (!infile.good()) {
throw Slic3r::RuntimeError(std::string("Load step file failed.\nCannot open file for reading.\n"));
throw Slic3r::RuntimeError(_u8L("Load step file failed.\nCannot open file for reading.\n"));
return false;
}
+75 -32
View File
@@ -51,6 +51,7 @@
#include <boost/spirit/home/karma.hpp>
#include <deque>
#include <boost/filesystem/directory.hpp>
#include <boost/system/errc.hpp>
#include <boost/thread/lock_guard.hpp>
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#include <boost/date_time/posix_time/posix_time_types.hpp>
@@ -987,6 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
MetadataList metadata;
VolumeMetadataList volumes;
bool auto_drop { true };
};
struct CutObjectInfo
@@ -1367,7 +1369,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_start_text_configuration(const char** attributes, unsigned int num_attributes);
bool _handle_start_shape_configuration(const char **attributes, unsigned int num_attributes);
bool _create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, const bool auto_drop, unsigned int recur_counter);
bool _create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter);
void _apply_transform(ModelInstance& instance, const Transform3d& transform);
@@ -2259,6 +2261,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (obj_metadata != m_objects_metadata.end()) {
// config data has been found, this model was saved using slic3r pe
model_object->auto_drop = obj_metadata->second.auto_drop;
// apply object's name and config data
for (const Metadata& metadata : obj_metadata->second.metadata) {
if (metadata.key == "name")
@@ -4117,9 +4121,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR);
Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR));
int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR);
int auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
return !m_load_model || _create_object_instance(path, object_id, transform, printable, auto_drop, 1);
return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1);
}
bool _BBS_3MF_Importer::_handle_end_item()
@@ -4347,7 +4349,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Importer::_create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, const bool auto_drop, unsigned int recur_counter)
bool _BBS_3MF_Importer::_create_object_instance(std::string const & path, int object_id, const Transform3d& transform, const bool printable, unsigned int recur_counter)
{
static const unsigned int MAX_RECURSIONS = 10;
@@ -4384,7 +4386,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
instance->printable = printable;
instance->auto_drop = auto_drop;
m_instances.emplace_back(instance, transform);
@@ -4417,7 +4418,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
instance->printable = printable;
instance->auto_drop = auto_drop;
m_instances.emplace_back(instance, transform);
}
@@ -4488,7 +4488,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// Added because of github #3435, currently not used by PrusaSlicer
// int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR);
m_objects_metadata.insert({ object_id, ObjectMetadata() });
ObjectMetadata metadata;
metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR);
m_objects_metadata.insert({ object_id, std::move(metadata) });
m_curr_config.object_id = object_id;
return true;
}
@@ -6164,14 +6166,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
unsigned int id;
Transform3d transform;
bool printable;
bool auto_drop;
BuildItem(std::string const & path, unsigned int id, const Transform3d& transform, const bool printable, const bool auto_drop)
BuildItem(std::string const & path, unsigned int id, const Transform3d& transform, const bool printable)
: path(path)
, id(id)
, transform(transform)
, printable(printable)
, auto_drop(auto_drop)
{
}
};
@@ -6262,7 +6262,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::vector<std::string> const &targets = {},
std::vector<std::string> const &types = {},
PackingTemporaryData data = PackingTemporaryData(),
int export_plate_idx = -1) const;
int export_plate_idx = -1,
bool plate_thumbnail_written = false,
bool plate_small_thumbnail_written = false) const;
bool _add_model_file_to_archive(const std::string& filename, mz_zip_archive& archive, const Model& model, ObjectToObjectDataMap& objects_data, Export3mfProgressFn proFn = nullptr, BBLProject* project = nullptr) const;
bool _add_object_to_model_stream(mz_zip_writer_staged_context &context, ObjectData const &object_data) const;
void _add_object_components_to_stream(std::stringstream &stream, ObjectData const &object_data) const;
@@ -6472,6 +6474,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(",top&&pick thumbnails, count %1%")%top_thumbnail_data.size();
//BBS: add thumbnail for each plate
//ORCA: the relationships file points at plate 1's thumbnail for a whole-project export,
// otherwise at the exported plate's. Track whether that thumbnail was written.
const size_t rels_plate_idx = (export_plate_idx < 0) ? 0 : static_cast<size_t>(export_plate_idx);
bool rels_thumbnail_written = false;
bool rels_small_thumbnail_written = false;
if (!m_skip_static) {
std::vector<bool> thumbnail_status(plate_data_list.size(), false);
std::vector<bool> no_light_thumbnail_status(plate_data_list.size(), false);
@@ -6520,6 +6527,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(",add thumbnail %1%'s data into 3mf")%(index+1);
thumbnail_status[index] = true;
if (static_cast<size_t>(index) == rels_plate_idx) {
//ORCA: this call also writes the _small companion
rels_thumbnail_written = true;
rels_small_thumbnail_written = true;
}
}
}
@@ -6530,7 +6542,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(",add no light thumbnail %1%'s data into 3mf") % (index + 1);
thumbnail_status[index] = true;
no_light_thumbnail_status[index] = true;
}
}
// Adds the file Metadata/top_i.png and Metadata/pick_i.png
@@ -6566,6 +6578,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":" << __LINE__ << boost::format(", add thumbnail %1% from file %2% failed\n") % (i+1) %plate_data->thumbnail_file;
return false;
}
//ORCA: copied from file, so there is no _small companion for this plate
if (static_cast<size_t>(i) == rels_plate_idx)
rels_thumbnail_written = true;
}
if (!no_light_thumbnail_status[i] && !plate_data->no_light_thumbnail_file.empty() && (boost::filesystem::exists(plate_data->no_light_thumbnail_file))){
@@ -6860,7 +6875,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// Adds relationships file ("_rels/.rels").
// The content of this file is the same for each OrcaSlicer 3mf.
// The relationshis file contains a reference to the geometry file "3D/3dmodel.model", the name was chosen to be compatible with CURA.
if (!_add_relationships_file_to_archive(archive, {}, {}, {}, temp_data, export_plate_idx)) {
if (!_add_relationships_file_to_archive(archive, {}, {}, {}, temp_data, export_plate_idx, rels_thumbnail_written, rels_small_thumbnail_written)) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", _add_relationships_file_to_archive failed\n");
return false;
}
@@ -7035,7 +7050,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
bool _BBS_3MF_Exporter::_add_relationships_file_to_archive(
mz_zip_archive &archive, std::string const &from, std::vector<std::string> const &targets, std::vector<std::string> const &types, PackingTemporaryData data, int export_plate_idx) const
mz_zip_archive &archive, std::string const &from, std::vector<std::string> const &targets, std::vector<std::string> const &types, PackingTemporaryData data, int export_plate_idx,
bool plate_thumbnail_written, bool plate_small_thumbnail_written) const
{
std::stringstream stream;
stream << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
@@ -7046,7 +7062,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (export_plate_idx < 0) {
//use cover image if have
if (data._3mf_thumbnail.empty()) {
stream << " <Relationship Target=\"/Metadata/plate_1.png"
// ORCA: only reference the plate thumbnail if it was written
if (plate_thumbnail_written)
stream << " <Relationship Target=\"/Metadata/plate_1.png"
<< "\" Id=\"rel-2\" Type=\"http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail\"/>\n";
} else {
stream << " <Relationship Target=\"/" << xml_escape(data._3mf_thumbnail)
@@ -7054,22 +7072,24 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
if (data._3mf_printer_thumbnail_middle.empty()) {
stream << " <Relationship Target=\"/Metadata/plate_1.png"
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
if (plate_thumbnail_written)
stream << " <Relationship Target=\"/Metadata/plate_1.png"
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
} else {
stream << " <Relationship Target=\"/" << xml_escape(data._3mf_printer_thumbnail_middle)
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
}
if (data._3mf_printer_thumbnail_small.empty()) {
stream << "<Relationship Target=\"/Metadata/plate_1_small.png"
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
if (plate_small_thumbnail_written)
stream << "<Relationship Target=\"/Metadata/plate_1_small.png"
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
} else {
stream << " <Relationship Target=\"/" << xml_escape(data._3mf_printer_thumbnail_small)
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
}
}
else {
else if (plate_thumbnail_written) {
//always use plate thumbnails
std::string thumbnail_file_str = (boost::format("Metadata/plate_%1%.png") % (export_plate_idx + 1)).str();
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
@@ -7079,9 +7099,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
<< "\" Id=\"rel-4\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-middle\"/>\n";
thumbnail_file_str = (boost::format("Metadata/plate_%1%_small.png") % (export_plate_idx + 1)).str();
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
if (plate_small_thumbnail_written) {
thumbnail_file_str = (boost::format("Metadata/plate_%1%_small.png") % (export_plate_idx + 1)).str();
stream << " <Relationship Target=\"/" << xml_escape(thumbnail_file_str)
<< "\" Id=\"rel-5\" Type=\"http://schemas.bambulab.com/package/2021/cover-thumbnail-small\"/>\n";
}
}
}
else if (targets.empty()) {
@@ -7390,7 +7412,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// instance_id is just a 1 indexed index in build_items.
//assert(m_skip_static || curr_id == build_items.size() + 1);
build_items.emplace_back("", object_it->second.object_id, t, instance->printable, instance->auto_drop);
build_items.emplace_back("", object_it->second.object_id, t, instance->printable);
count++;
}
@@ -7840,8 +7862,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << "\" " << PPATH_ATTR << "=\"" << xml_escape(item.path);
stream << "\" " << TRANSFORM_ATTR << "=\"";
add_transformation(stream, item.transform);
stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable;
stream << "\" " << AUTO_DROP_ATTR << "=\"" << item.auto_drop << "\"/>\n";
stream << "\" " << PRINTABLE_ATTR << "=\"" << item.printable << "\"/>\n";
}
stream << " </" << BUILD_TAG << ">\n";
@@ -8278,7 +8299,8 @@ bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archiv
if (obj != nullptr) {
// Output of instances count added because of github #3435, currently not used by PrusaSlicer
//stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << obj_metadata.first << "\" " << INSTANCESCOUNT_ATTR << "=\"" << obj->instances.size() << "\">\n";
stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << object_data.object_id << "\">\n";
stream << " <" << OBJECT_TAG << " " << ID_ATTR << "=\"" << object_data.object_id
<< "\" " << AUTO_DROP_ATTR << "=\"" << obj->auto_drop << "\">\n";
// stores object's name
if (!obj->name.empty())
@@ -9031,6 +9053,16 @@ bool _BBS_3MF_Exporter::_add_gcode_file_to_archive(mz_zip_archive& archive, cons
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", store %1% to 3mf %2%\n") % src_gcode_file % gcode_in_3mf;
}
});
for (const PlateData *plate_data : plate_data_list2) {
if (plate_data->statistics.empty())
continue;
const std::string statistics_in_3mf = (boost::format(STATISTICS_FILE_FORMAT) % (plate_data->plate_index + 1)).str();
if (!mz_zip_writer_add_mem(&archive, statistics_in_3mf.c_str(), plate_data->statistics.data(), plate_data->statistics.size(),
MZ_DEFAULT_COMPRESSION)) {
add_error("Unable to add statistics file to archive");
result = false;
}
}
return result;
}
@@ -9755,10 +9787,13 @@ bool has_restore_data(std::string & path, std::string& origin)
origin = "<lock>";
return false;
}
if (boost::filesystem::exists(path + "/lock.txt")) {
// ORCA: the caller removes this directory unless origin is "<lock>", so every way of
// failing to establish the backup's state has to claim the lock instead.
boost::system::error_code ec;
if (boost::filesystem::exists(path + "/lock.txt", ec)) {
std::string pid;
load_string_file(path + "/lock.txt", pid);
try {
load_string_file(path + "/lock.txt", pid);
if (get_process_name(boost::lexical_cast<int>(pid)) ==
get_process_name(0)) {
origin = "<lock>";
@@ -9766,14 +9801,22 @@ bool has_restore_data(std::string & path, std::string& origin)
}
}
catch (...) {
origin = "<lock>";
return false;
}
}
std::string file3mf = path + "/.3mf";
if (!boost::filesystem::exists(file3mf))
if (!boost::filesystem::exists(file3mf, ec)) {
// ORCA: a .3mf that is simply absent is a spent backup and stays collectable. A probe
// that could not look must not be: ec reports ENOENT/ENOTDIR for "not there" as
// well, so only a different error means the answer is unknown.
if (ec && ec != boost::system::errc::no_such_file_or_directory &&
ec != boost::system::errc::not_a_directory)
origin = "<lock>";
return false;
}
try {
if (boost::filesystem::exists(path + "/origin.txt"))
if (boost::filesystem::exists(path + "/origin.txt", ec))
load_string_file(path + "/origin.txt", origin);
}
catch (...) {
+5
View File
@@ -32,6 +32,7 @@ struct ThumbnailData;
#define PLATE_THUMBNAIL_SMALL_HEIGHT 128
#define GCODE_FILE_FORMAT "Metadata/plate_%1%.gcode"
#define STATISTICS_FILE_FORMAT "Metadata/plate_%1%.statistics.json"
#define THUMBNAIL_FILE_FORMAT "Metadata/plate_%1%.png"
#define NO_LIGHT_THUMBNAIL_FILE_FORMAT "Metadata/plate_no_light_%1%.png"
#define TOP_FILE_FORMAT "Metadata/top_%1%.png"
@@ -100,6 +101,8 @@ struct PlateData
std::string nozzle_volume_types;
std::string gcode_file;
std::string gcode_file_md5;
// The G-code's statistics as JSON, stored with the G-code when not empty.
std::string statistics;
std::string thumbnail_file;
std::string no_light_thumbnail_file;
ThumbnailData plate_thumbnail;
@@ -174,6 +177,8 @@ enum class SaveStrategy
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
// production-extension value.
MinimalPublished = 1 << 13,
// With WithGcode, also the G-code's statistics.
WithStatistics = 1 << 14,
SplitModel = 0x1000 | ProductionExt,
Encrypted = SecureContentExt | SplitModel,
+3 -2
View File
@@ -4,6 +4,7 @@
#include "../libslic3r.h"
#include "../Model.hpp"
#include "../TriangleMesh.hpp"
#include "../I18N.hpp"
#include "svg.hpp"
#include "nanosvg/nanosvg.h"
@@ -143,11 +144,11 @@ bool get_svg_profile(const char *path, std::vector<Element_Info> &element_infos,
NSVGimage *svg_data = nullptr;
svg_data = nsvgParseFromFile(path, "mm", 96.0f);
if (svg_data == nullptr) {
message = "import svg failed: could not open svg.";
message = _u8L("import svg failed: could not open svg.");
return false;
}
if (svg_data->shapes == nullptr) {
message = "import svg failed: could not parse imported svg data.";
message = _u8L("import svg failed: could not parse imported svg data.");
return false;
}
+53 -29
View File
@@ -1979,7 +1979,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) {
if (m_layer_idx < (int) m_tool_changes.size()) {
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
throw Slic3r::RuntimeError(_(L("Wipe tower generation failed, possibly due to empty first layer.")));
// Calculate where the wipe tower layer will be printed. -1 means that print z will not change,
// resulting in a wipe tower with sparse layers.
@@ -2018,7 +2018,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) {
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
throw Slic3r::RuntimeError(_(L("Wipe tower generation failed, possibly due to empty first layer.")));
if (!ignore_sparse)
gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z);
@@ -2044,7 +2044,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (gcodegen.writer().need_toolchange(extruder_id) || finish_layer) {
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
throw Slic3r::RuntimeError(_(L("Wipe tower generation failed, possibly due to empty first layer.")));
if (!ignore_sparse) {
return false;
@@ -2625,8 +2625,15 @@ static void set_mass_locator(GCodeProcessor &processor, const Print &print)
}
Object &o = objects.emplace_back(Object{ object, int(object_masses.size()), count, bodies_total, {}, std::move(bodies), {}, {} });
object_masses.resize(object_masses.size() + object->instances().size());
for (size_t instance = 0; instance < object->instances().size(); ++instance)
object_masses[o.first_instance + instance].assembly = assembly;
const ModelInstancePtrs &model_instances = object->model_object()->instances;
for (size_t instance = 0; instance < object->instances().size(); ++instance) {
GCodeProcessorResult::ObjectMass &mass = object_masses[o.first_instance + instance];
mass.assembly = assembly;
mass.name = object->model_object()->name;
// Copies of another rotation or scale slice as objects of their own, so number them in the model object.
mass.instance = int(std::find(model_instances.begin(), model_instances.end(), object->instances()[instance].model_instance) -
model_instances.begin());
}
bodies_total += int(count * object->instances().size());
for (const Layer *layer : layers) {
o.print_zs.emplace_back(layer->print_z);
@@ -2792,11 +2799,13 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
m_processor.set_print(print);
GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor);
if (! file.is_open()) {
std::string err_msg = std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n";
// TRN %1% is a file path.
std::string err_msg = Slic3r::format(_(L("G-code export to %1% failed.\nCannot open the file for writing.")), path) + "\n";
BOOST_LOG_TRIVIAL(error) << err_msg << std::endl;
if (!fs::exists(folder)) {
//fs::create_directory(folder);
std::string add_err_msg = "the parent path " + folder.string() +" is not there!!!";
// TRN %1% is a folder path.
std::string add_err_msg = Slic3r::format(_(L("The folder %1% does not exist.")), folder.string());
BOOST_LOG_TRIVIAL(error) << add_err_msg << std::endl;
err_msg += add_err_msg;
}
@@ -2818,7 +2827,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
if (file.is_error()) {
file.close();
boost::nowide::remove(path_tmp.c_str());
throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed\nIs the disk full?\n");
// TRN %1% is a file path.
throw Slic3r::RuntimeError(Slic3r::format(_(L("G-code export to %1% failed.\nIs the disk full?")), path) + "\n");
}
} catch (std::exception &ex) {
// Rethrow on any exception. std::runtime_exception and CanceledException are expected to be thrown.
@@ -2949,8 +2959,9 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to rename the output G-code file from ") + path_tmp + " to " + path + '\n' + "error code " + ret.message() + '\n' +
"Is " + path_tmp + " locked?" + '\n');
// TRN %1% is the temporary file path, %2% the output file path, %3% the system error message.
Slic3r::format(_(L("Failed to rename the output G-code file from %1% to %2%.\nError: %3%\nIs %1% locked?")),
path_tmp, path, ret.message()) + '\n');
}
else {
BOOST_LOG_TRIVIAL(info) << boost::format("rename_file from %1% to %2% successfully")% path_tmp % path;
@@ -3559,6 +3570,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
print.throw_if_canceled();
// Taken before the object loop, which reassigns initial_extruder_id when printing by object.
const int compat_first_layer_bed_temp = m_config.bed_temperature_formula == BedTempFormula::btfHighestTemp ?
get_highest_bed_temperature(true, print) :
get_bed_temperature(initial_extruder_id, true, print.config().curr_bed_type);
const int compat_first_layer_nozzle_temp =
print.config().nozzle_temperature_initial_layer.get_at(get_filament_config_index((int) initial_extruder_id));
int extruder_id = get_extruder_id(initial_extruder_id);
m_cooling_buffer = make_unique<CoolingBuffer>(*this);
@@ -4149,19 +4167,15 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
file.write(full_config);
// SoftFever: write compatiple image
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n",
first_layer_bed_temperature);
file.write_format(
"; first_layer_temperature = %d\n",
print.config().nozzle_temperature_initial_layer.get_at(0));
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
file.write("; CONFIG_BLOCK_END\n\n");
} else if (thumbnail_cb != nullptr) {
// generate the thumbnails
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = format("Invalid thumbnails value:");
std::string error_str = _(L("Invalid thumbnails value:"));
error_str += GCodeThumbnails::get_error_string(errors);
throw Slic3r::ExportError(error_str);
}
@@ -4197,6 +4211,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
file.write_format("; EXECUTABLE_BLOCK_START\n");
// Set for every printer, as the object label comments use these ids too.
assign_object_label_ids(print);
// SoftFever
if( m_enable_exclude_object)
file.write(set_object_info(&print));
@@ -4737,10 +4753,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
file.write(full_config);
// SoftFever: write compatiple info
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
file.write_format("; first_layer_bed_temperature = %d\n", compat_first_layer_bed_temp);
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
file.write_format("; first_layer_temperature = %d\n", compat_first_layer_nozzle_temp);
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
//SF TODO
@@ -11051,15 +11066,30 @@ inline std::string polygon_to_string(const Polygon &polygon, Print *print, bool
gcode << "]";
return gcode.str();
}
// this function iterator PrintObject and assign a seqential id to each object.
// this id is used to generate unique object id for each object.
// Numbers the objects, and their instances both within each object (id) and across the print
// (unique_id). The "; printing object" comments and the exclude-object commands label objects with these ids.
void GCode::assign_object_label_ids(Print &print)
{
m_instance_names.clear();
size_t object_id = 0;
size_t unique_id = 0;
for (PrintObject *object : print.objects()) {
object->set_id(object_id++);
size_t inst_id = 0;
for (PrintInstance &inst : object->instances()) {
inst.id = inst_id++;
inst.unique_id = unique_id++;
}
}
}
// Writes the exclude-object definitions for the ids assign_object_label_ids() set.
std::string GCode::set_object_info(Print *print) {
const auto gflavor = print->config().gcode_flavor.value;
if (print->is_BBL_printer() ||
(gflavor != gcfKlipper && gflavor != gcfMarlinLegacy && gflavor != gcfMarlinFirmware && gflavor != gcfRepRapFirmware))
return "";
std::ostringstream gcode;
size_t object_id = 0;
// Orca: check if we are in pa calib mode
if (print->calib_mode() == CalibMode::Calib_PA_Pattern) {
BoundingBoxf bbox_bed(print->config().printable_area.values);
@@ -11075,14 +11105,8 @@ std::string GCode::set_object_info(Print *print) {
} else if (print->calib_mode() == CalibMode::Calib_PA_Line) {
// PA_Line has only one object, no EXCLUDE_OBJECT_DEFINE needed
} else {
size_t unique_id = 0;
m_instance_names.clear();
for (PrintObject* object : print->objects()) {
object->set_id(object_id++);
size_t inst_id = 0;
for (PrintInstance& inst : object->instances()) {
inst.unique_id = unique_id++;
inst.id = inst_id++;
// Outlines are in plate coordinates. On a belt printer that is the frame after
// the slicing rotation has been undone and before the G-code axis remap and
// machine-frame shear: where the object stands on the belt, which is what an
+2 -1
View File
@@ -319,6 +319,7 @@ public:
bool is_BBL_Printer();
WipeTowerType wipe_tower_type();
void assign_object_label_ids(Print &print);
// SoftFever
std::string set_object_info(Print* print);
@@ -794,7 +795,7 @@ protected:
bool m_enable_exclude_object;
std::vector<size_t> m_label_objects_ids;
// Object label names by instance, built on first use from the ids set_object_info() assigns.
// Object label names by instance, built on first use from the ids assign_object_label_ids() assigns.
std::unordered_map<const PrintInstance*, std::string> m_instance_names;
const std::string& instance_name(const PrintInstance &instance);
std::string _encode_label_ids_to_base64(std::vector<size_t> ids);
+11 -11
View File
@@ -22,6 +22,8 @@
#include "libslic3r/Config.hpp"
#include "libslic3r/GCodeWriter.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/format.hpp"
namespace Slic3r {
@@ -321,8 +323,7 @@ std::string AdaptivePAProcessor::validate_adaptive_pa_model(const std::string& m
// Only numbers, commas and dots are allowed (no letters or other characters)
for (char c : line) {
if (!std::isdigit(static_cast<unsigned char>(c)) && c != ',' && c != '.') {
return "Line " + std::to_string(line_number) +
": only numbers, commas and dots are allowed";
return Slic3r::format(_u8L("Line %1%: only numbers, commas and dots are allowed"), line_number);
}
}
@@ -333,8 +334,7 @@ std::string AdaptivePAProcessor::validate_adaptive_pa_model(const std::string& m
}
if (comma_count != 2) {
return "Line " + std::to_string(line_number) +
": must contain exactly 3 comma-separated values (PA, flow, acceleration)";
return Slic3r::format(_u8L("Line %1%: must contain exactly 3 comma-separated values (PA, flow, acceleration)"), line_number);
}
// Parse and validate the values
@@ -344,31 +344,31 @@ std::string AdaptivePAProcessor::validate_adaptive_pa_model(const std::string& m
// Parse PA
if (!std::getline(line_stream, value, ','))
return "Line " + std::to_string(line_number) + ": missing PA value";
return Slic3r::format(_u8L("Line %1%: missing PA value"), line_number);
double pa = std::stod(value);
// Parse flow
if (!std::getline(line_stream, value, ','))
return "Line " + std::to_string(line_number) + ": missing flow value";
return Slic3r::format(_u8L("Line %1%: missing flow value"), line_number);
double flow = std::stod(value);
// Parse acceleration
if (!std::getline(line_stream, value, ','))
return "Line " + std::to_string(line_number) + ": missing acceleration value";
return Slic3r::format(_u8L("Line %1%: missing acceleration value"), line_number);
double accel = std::stod(value);
// Validate constraints
if (pa >= 2.0) {
return "Line " + std::to_string(line_number) + ": PA value must be less than 2";
return Slic3r::format(_u8L("Line %1%: PA value must be less than 2"), line_number);
}
if (flow <= pa) {
return "Line " + std::to_string(line_number) + ": flow value must be greater than PA value";
return Slic3r::format(_u8L("Line %1%: flow value must be greater than PA value"), line_number);
}
if (accel <= flow) {
return "Line " + std::to_string(line_number) + ": acceleration value must be greater than flow value";
return Slic3r::format(_u8L("Line %1%: acceleration value must be greater than flow value"), line_number);
}
} catch (const std::exception&) {
return "Line " + std::to_string(line_number) + ": invalid numeric value";
return Slic3r::format(_u8L("Line %1%: invalid numeric value"), line_number);
}
}
+25 -23
View File
@@ -18,6 +18,7 @@
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/LocalesUtils.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r_version.h"
#include "GCodeProcessor.hpp"
@@ -1066,13 +1067,13 @@ void GCodeProcessor::run_post_process()
{
FilePtr in{ boost::nowide::fopen(m_result.filename.c_str(), "rb") };
if (in.f == nullptr)
throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for reading.\n"));
throw Slic3r::RuntimeError(_u8L("G-code processor post-process export failed.\nCannot open file for reading.\n"));
// temporary file to contain modified gcode
std::string out_path = m_result.filename + ".postprocess";
FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") };
if (out.f == nullptr)
throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for writing.\n"));
throw Slic3r::RuntimeError(_u8L("G-code processor post-process export failed.\nCannot open file for writing.\n"));
std::vector<double> filament_mm(m_result.filaments_count, 0.0);
std::vector<double> filament_cm3(m_result.filaments_count, 0.0);
@@ -1467,10 +1468,10 @@ void GCodeProcessor::run_post_process()
// found an invalid value, clamp it to a valid one
tool_number = std::clamp<int>(0, m_filament_nozzle_temp.size() - 1, tool_number);
// emit warning
std::string warning = "GCode Post-Processor encountered an invalid toolchange, maybe from a custom gcode:";
std::string warning = _u8L("The G-code post-processor encountered an invalid tool change, maybe from custom G-code:");
warning += "\n> ";
warning += gcode_line;
warning += "Generated M104 lines may be incorrect.";
warning += _u8L("Generated M104 lines may be incorrect.");
BOOST_LOG_TRIVIAL(error) << warning;
// Orca todo
if (m_print != nullptr)
@@ -1712,7 +1713,7 @@ void GCodeProcessor::run_post_process()
for (;;) {
size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f);
if (::ferror(in.f))
throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nError while reading from file.\n"));
throw Slic3r::RuntimeError(_u8L("G-code processor post-process export failed.\nError while reading from file.\n"));
bool eof = cnt_read == 0;
auto it = buffer.begin();
auto it_bufend = buffer.begin() + cnt_read;
@@ -1877,8 +1878,9 @@ void GCodeProcessor::run_post_process()
}
if (rename_file(out_path, result_filename))
throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + result_filename + '\n' +
"Is " + out_path + " locked?" + '\n');
// TRN %1% is the temporary file path, %2% the output file path.
throw Slic3r::RuntimeError(Slic3r::format(_u8L("Failed to rename the output G-code file from %1% to %2%.\nIs %1% locked?"),
out_path, result_filename) + '\n');
}
// Additive second file-rewrite pass for the pre-heat/pre-cool injector. The single-pass
@@ -1979,12 +1981,12 @@ void GCodeProcessor::run_second_pass_injection()
FilePtr in{ boost::nowide::fopen(m_result.filename.c_str(), "rb") };
if (in.f == nullptr)
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nCannot open file for reading.\n"));
throw Slic3r::RuntimeError(_u8L("G-code processor pre-heat injection pass failed.\nCannot open file for reading.\n"));
const std::string out_path = m_result.filename + ".preheat";
FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") };
if (out.f == nullptr)
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nCannot open file for writing.\n"));
throw Slic3r::RuntimeError(_u8L("G-code processor pre-heat injection pass failed.\nCannot open file for writing.\n"));
// The rewrite may shift byte positions (once the injector inserts lines), so rebuild lines_ends from scratch.
// With an empty map the scanned '\n' offsets reproduce the current lines_ends exactly.
@@ -2002,7 +2004,7 @@ void GCodeProcessor::run_second_pass_injection()
for (;;) {
size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f);
if (::ferror(in.f))
throw Slic3r::RuntimeError(std::string("GCode processor pre-heat injection pass failed.\nError while reading from file.\n"));
throw Slic3r::RuntimeError(_u8L("G-code processor pre-heat injection pass failed.\nError while reading from file.\n"));
bool eof = cnt_read == 0;
auto it = buffer.begin();
auto it_bufend = buffer.begin() + cnt_read;
@@ -2053,8 +2055,9 @@ void GCodeProcessor::run_second_pass_injection()
handle_offsets_of_second_process(inserted_operation_lines);
if (rename_file(out_path, m_result.filename))
throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + m_result.filename + '\n' +
"Is " + out_path + " locked?" + '\n');
// TRN %1% is the temporary file path, %2% the output file path.
throw Slic3r::RuntimeError(Slic3r::format(_u8L("Failed to rename the output G-code file from %1% to %2%.\nIs %1% locked?"),
out_path, m_result.filename) + '\n');
}
// Every move whose gcode_id sits at or after an inserted line is pushed down by the running count of
@@ -3727,6 +3730,7 @@ void GCodeProcessor::reset()
m_detect_layer_based_on_tag = false;
m_seams_count = 0;
m_g29_time_added = false;
m_preheat_time = 0.f;
m_preheat_steps = 1;
}
@@ -3748,7 +3752,8 @@ DynamicConfig GCodeProcessor::export_config_for_render() const
config.set_key_value("filament_is_support", new ConfigOptionBools(m_parser.get_config().filament_is_support.values));
config.set_key_value("filament_type", new ConfigOptionStrings(m_parser.get_config().filament_type.values));
config.set_key_value("filament_map", new ConfigOptionInts(m_parser.get_config().filament_map.values));
config.apply(m_belt_render_config);
// += clones the options; apply() would need a definition this config does not have.
config += m_belt_render_config;
return config;
}
@@ -3941,7 +3946,7 @@ ConfigSubstitutions load_from_superslicer_gcode_file(const std::string& filename
key_value_pairs = ConfigBase::load_from_gcode_string_legacy(config, data.data(), substitutions_ctxt);
if (key_value_pairs < 80)
throw Slic3r::RuntimeError(format("Suspiciously low number of configuration values extracted from %1%: %2%", filename, key_value_pairs));
throw Slic3r::RuntimeError(format(_u8L("Suspiciously low number of configuration values extracted from %1%: %2%"), filename, key_value_pairs));
return std::move(substitutions_ctxt.substitutions);
}
@@ -6128,15 +6133,12 @@ void GCodeProcessor::process_G4(const GCodeReader::GCodeLine& line)
//BBS
void GCodeProcessor::process_G29(const GCodeReader::GCodeLine& line)
{
//BBS: hardcode 260 seconds for G29
//Todo: use a machine related setting when we have second kind of BBL printer
const float value_s = 260.0;
if (s_IsBBLPrinter){
if(m_measure_g29_time)
simulate_st_synchronize(value_s);
}
else{
simulate_st_synchronize(value_s);
// Firmware may split one bed-leveling sequence across multiple G29 subcommands.
constexpr float g29_time = 260.0f;
const bool should_add_time = !s_IsBBLPrinter || m_measure_g29_time;
if (should_add_time && !m_g29_time_added) {
simulate_st_synchronize(g29_time);
m_g29_time_added = true;
}
}
+4
View File
@@ -298,6 +298,9 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
BoundingBoxf3 box;
// Of an object, whether it is an assembly.
bool assembly{ false };
// Of an object instance, its model object's name and its index among that object's instances.
std::string name;
int instance{ -1 };
Sum total() const { return printed_up_to_layer.empty() ? Sum{} : printed_up_to_layer.back(); }
void add(const Sum &sum, const BoundingBoxf3 &extent, size_t layer);
@@ -1280,6 +1283,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
bool m_detect_layer_based_on_tag {false};
int m_seams_count;
bool m_measure_g29_time {false};
bool m_g29_time_added {false};
bool m_single_extruder_multi_material;
float m_preheat_time;
int m_preheat_steps;
+200
View File
@@ -0,0 +1,200 @@
#include "GCodeStatistics.hpp"
#include "GCodeProcessor.hpp"
#include "libslic3r/Exception.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/libslic3r.h"
#include "libslic3r_version.h"
#include <boost/filesystem/path.hpp>
#include <boost/nowide/fstream.hpp>
#include <nlohmann/json.hpp>
#include <cmath>
#include <cstddef>
#include <ios>
#include <map>
#include <math.h>
#include <string>
#include <vector>
namespace Slic3r {
namespace {
using json = nlohmann::ordered_json;
double rounded(double value, int decimals)
{
const double scale = std::pow(10., decimals);
return std::round(value * scale) / scale;
}
// As the object masses weigh it, at the default density for a filament without one.
double weight_of(const GCodeProcessorResult &result, size_t filament, double volume)
{
const bool has_density = filament < result.filament_densities.size() && result.filament_densities[filament] > 0.f;
// mm³ at g/cm³ weigh mg.
return 0.001 * volume * (has_density ? result.filament_densities[filament] : DEFAULT_FILAMENT_DENSITY);
}
struct Usage
{
double time{ 0. };
double volume{ 0. };
double weight{ 0. };
void add(const Usage &other)
{
time += other.time;
volume += other.volume;
weight += other.weight;
}
json to_json() const { return json{ { "time", rounded(time, 3) }, { "volume", rounded(volume, 3) }, { "weight", rounded(weight, 4) } }; }
};
json features_json(const std::map<ExtrusionRole, Usage> &features)
{
json out = json::object();
for (const auto &[role, usage] : features)
out[ExtrusionEntity::role_to_string(role)] = usage.to_json();
return out;
}
} // namespace
std::string gcode_statistics_json(const GCodeProcessorResult &result)
{
constexpr size_t normal = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
struct Layer
{
float z{ 0.f };
double travel_time{ 0. };
Usage total;
std::map<ExtrusionRole, Usage> features;
};
std::vector<Layer> layers;
std::map<ExtrusionRole, Usage> features;
double travel_time = 0.;
// As the preview sums them: a layer takes the time of all its moves, a feature that of its extrusions.
for (const GCodeProcessorResult::MoveVertex &move : result.moves) {
if (layers.size() <= move.layer_id)
layers.resize(move.layer_id + 1);
Layer &layer = layers[move.layer_id];
const double time = move.time[normal];
layer.total.time += time;
if (move.type == EMoveType::Travel) {
layer.travel_time += time;
travel_time += time;
}
// A vertex added inside a move to show its acceleration repeats the move's extrusion.
if (move.type != EMoveType::Extrude || move.internal_only)
continue;
if (move.extrusion_role != erCustom)
layer.z = move.position.z();
Usage usage;
usage.time = time;
usage.volume = double(move.delta_extruder) * M_PI * sqr(0.5 * double(result.filament_diameters[move.extruder_id]));
usage.weight = weight_of(result, move.extruder_id, usage.volume);
layer.total.volume += usage.volume;
layer.total.weight += usage.weight;
layer.features[move.extrusion_role].add(usage);
features[move.extrusion_role].add(usage);
}
// As the preview's filament table, which counts the flush too.
const PrintEstimatedStatistics &stats = result.print_statistics;
double volume = 0., weight = 0., cost = 0.;
json filaments_json = json::array();
for (const auto &entry : stats.total_volumes_per_extruder) {
const size_t filament = entry.first;
const auto use = [&result, filament](const std::map<size_t, double> &volumes) {
const auto it = volumes.find(filament);
const double used = it == volumes.end() ? 0. : it->second;
return json{ { "volume", rounded(used, 3) }, { "weight", rounded(weight_of(result, filament, used), 4) } };
};
const double filament_weight = weight_of(result, filament, entry.second);
// Priced per kilogram.
const double filament_cost = filament < result.filament_costs.size() ? 0.001 * filament_weight * result.filament_costs[filament] : 0.;
volume += entry.second;
weight += filament_weight;
cost += filament_cost;
filaments_json.push_back(json{ { "id", filament + 1 },
{ "length", rounded(entry.second / (M_PI * sqr(0.5 * double(result.filament_diameters[filament]))), 2) },
{ "volume", rounded(entry.second, 3) },
{ "weight", rounded(filament_weight, 4) },
{ "cost", rounded(filament_cost, 2) },
{ "model", use(stats.model_volumes_per_extruder) },
{ "support", use(stats.support_volumes_per_extruder) },
{ "prime_tower", use(stats.wipe_tower_volumes_per_extruder) },
{ "flush", use(stats.flush_per_filament) } });
}
json summary = json{ { "time", rounded(stats.modes[normal].time, 3) },
{ "prepare_time", rounded(stats.modes[normal].prepare_time, 3) },
{ "travel_time", rounded(travel_time, 3) },
{ "travel_distance", rounded(stats.total_travel_distance, 2) },
{ "layer_count", layers.size() },
{ "volume", rounded(volume, 3) },
{ "weight", rounded(weight, 4) },
{ "filament_cost", rounded(cost, 2) },
{ "filament_changes", stats.total_filament_changes },
{ "filaments", filaments_json },
{ "features", features_json(features) } };
json layers_json = json::array();
for (const Layer &layer : layers)
layers_json.push_back(json{ { "z", rounded(layer.z, 3) },
{ "time", rounded(layer.total.time, 3) },
{ "travel_time", rounded(layer.travel_time, 3) },
{ "volume", rounded(layer.total.volume, 3) },
{ "weight", rounded(layer.total.weight, 4) },
{ "features", features_json(layer.features) } });
// The supports of an instance are numbered as the instance.
json objects_json = json::array();
for (size_t i = 0; i < result.object_masses.size(); ++i) {
const GCodeProcessorResult::ObjectMass &object = result.object_masses[i];
const GCodeProcessorResult::ObjectMass::Sum part = object.total();
const GCodeProcessorResult::ObjectMass::Sum support = i < result.support_masses.size() ? result.support_masses[i].total() :
GCodeProcessorResult::ObjectMass::Sum{};
// In mg, as volumes in mm³ at g/cm³.
objects_json.push_back(json{ { "name", object.name },
{ "instance", object.instance },
{ "volume", rounded(part.volume, 3) },
{ "weight", rounded(0.001 * part.mass, 4) },
{ "support_volume", rounded(support.volume, 3) },
{ "support_weight", rounded(0.001 * support.mass, 4) } });
}
// The version goes up only when a key is removed, renamed or changes meaning.
const json out = json{ { "version", 1 },
{ "generator", std::string(SLIC3R_APP_NAME) + " " + SoftFever_VERSION },
{ "units", json{ { "time", "s" }, { "length", "mm" }, { "volume", "mm3" }, { "weight", "g" } } },
{ "summary", summary },
{ "layers", layers_json },
{ "objects", objects_json } };
return out.dump(2);
}
std::string gcode_statistics_path(const std::string &gcode_path)
{
return boost::filesystem::path(gcode_path).replace_extension(".statistics.json").string();
}
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path)
{
// Binary, so the file reads the same on every platform and inside a 3MF.
boost::nowide::ofstream file(path, std::ios::out | std::ios::binary | std::ios::trunc);
file << gcode_statistics_json(result);
file.close();
if (file.fail())
throw ExportError(format(_u8L("Failed to write the G-code statistics to %1%."), path));
}
} // namespace Slic3r
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <string>
namespace Slic3r {
struct GCodeProcessorResult;
// The statistics of a plate's G-code as JSON, in seconds, millimeters and grams: totals, per layer and per object instance.
std::string gcode_statistics_json(const GCodeProcessorResult &result);
// Where the statistics of the G-code at gcode_path are written.
std::string gcode_statistics_path(const std::string &gcode_path);
// Throws ExportError when the file cannot be written.
void export_gcode_statistics(const GCodeProcessorResult &result, const std::string &path);
} // namespace Slic3r
+23 -19
View File
@@ -673,7 +673,7 @@ static void refine_at_vertex(
}
}
// Records one extraction for the warnings: the type of a modifier with a discarded fragment, and the
// Records one extraction for the warnings: the type of a modifier with a discarded fragment, the
// evaluated/reached flags for "had no effect" (a discarded fragment counts as reached, a contact not).
static void record_extraction(PreciseSeamWarnings *warnings, const ModelVolume *modifier,
const SegmentExtraction &extracted)
@@ -685,13 +685,10 @@ static void record_extraction(PreciseSeamWarnings *warnings, const ModelVolume *
const auto it = warnings->modifier_usage.find(modifier);
if (it == warnings->modifier_usage.end())
return;
// Load before store: most calls find the flag already set, so shared cache lines stay clean.
PreciseSeamWarnings::ModifierUsage &usage = it->second;
if (!usage.checked.load(std::memory_order_relaxed))
usage.checked.store(true, std::memory_order_relaxed);
const bool reached = !extracted.segments.empty() || extracted.full_containment || extracted.discarded_fragments > 0;
if (reached && !usage.reached.load(std::memory_order_relaxed))
usage.reached.store(true, std::memory_order_relaxed);
PreciseSeamWarnings::set(usage.checked);
if (!extracted.segments.empty() || extracted.full_containment || extracted.discarded_fragments > 0)
PreciseSeamWarnings::set(usage.reached);
}
// Strong priority is per modifier, never a global maximum across different modifiers.
@@ -953,22 +950,15 @@ std::vector<WeakModifierSegment> collect_weak_modifier_segments(
const SegmentExtraction extracted = extract_perimeter_segments(prepared, it->second[layer_id], modifier_volume->type(), context);
record_extraction(warnings, modifier_volume, extracted);
const auto type = convert_weak_modifier_type(modifier_volume->type());
// Full containment works like seam painting: Enforced and Neutral type the whole perimeter;
// Blocked is skipped with a warning, since the seam cannot avoid the whole perimeter.
// Full containment works like seam painting all round: the whole perimeter takes the type.
// A perimeter left whole Blocked also gets a warning (see apply_weak_modifiers_to_perimeter()).
if (extracted.full_containment) {
if (type == EnforcedBlockedSeamPoint::Blocked) {
if (warnings)
PreciseSeamWarnings::mark(warnings->full_containment, modifier_volume->type());
continue;
}
WeakModifierSegment whole{type, Point(), PerimeterPosition{0, 0.}, Point(), PerimeterPosition{0, 0.}};
whole.whole_perimeter = true;
result.push_back(whole);
result.push_back({type, Point(), PerimeterPosition{0, 0.}, Point(), PerimeterPosition{0, 0.}, true, modifier_volume});
continue;
}
for (const PerimeterSegment &segment : extracted.segments) {
result.push_back({type, segment.polyline.points.front(), segment.begin,
segment.polyline.points.back(), segment.end});
segment.polyline.points.back(), segment.end, false, modifier_volume});
}
}
// Boundary insertion, refinement and last-write-wins priority use the established path.
@@ -979,7 +969,8 @@ void apply_weak_modifiers_to_perimeter(
const std::vector<WeakModifierSegment> &weak_segments,
PrintObjectSeamData::LayerSeams &result,
const SeamPlacerImpl::Perimeter &perimeter,
bool &some_point_enforced)
bool &some_point_enforced,
PreciseSeamWarnings *warnings)
{
// Get z-coordinate for unscaling boundary points
const float z_coord = result.points[perimeter.start_index].position.z();
@@ -1029,6 +1020,19 @@ void apply_weak_modifiers_to_perimeter(
some_point_enforced = true;
}
}
// Only the highest whole zone is in effect, and a higher band may still unblock part of it.
if (warnings == nullptr)
return;
const auto last_whole = std::find_if(weak_segments.rbegin(), weak_segments.rend(),
[](const WeakModifierSegment &segment) { return segment.whole_perimeter; });
if (last_whole == weak_segments.rend() || last_whole->type != EnforcedBlockedSeamPoint::Blocked)
return;
for (size_t i = perimeter.start_index; i < perimeter.end_index; ++i)
if (result.points[i].type != EnforcedBlockedSeamPoint::Blocked)
return;
if (const auto usage = warnings->modifier_usage.find(last_whole->modifier); usage != warnings->modifier_usage.end())
PreciseSeamWarnings::set(usage->second.fully_blocked);
}
// Restore precise seam positions that may have been modified
+20 -10
View File
@@ -49,18 +49,20 @@ struct PreciseSeamWarnings {
// Masks of the Precise Seam types that caused each warning reason, one bit per type (type_bit()).
// The user warning lists the types instead of naming modifiers.
std::atomic<unsigned> multiple_intersections{0}; // Center/Left/Right with several segments on a perimeter.
std::atomic<unsigned> full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right, Blocked.
std::atomic<unsigned> full_containment{0}; // Skipped for a perimeter fully inside: Center/Left/Right.
std::atomic<unsigned> failed_types{0}; // Types with at least one discarded fragment.
std::atomic<size_t> failed_fragments{0}; // Total discarded fragments, for the log summary.
// Fragments saved by the rare-case fallback or accepted as contacts; log only, no user warning.
// Clipper is deterministic, so a prismatic model can repeat the same case on every layer.
std::atomic<size_t> recovered_fragments{0};
// Per-modifier flags for the "had no effect" warning. Modifiers are registered before the parallel
// phase, so workers only set flags; unregistered ones (e.g. in tests) are not tracked.
// Per-modifier flags for the "had no effect" and "covers an entire perimeter" warnings. Modifiers are
// registered before the parallel phase, so workers only set flags; unregistered ones (e.g. in tests)
// are not tracked.
struct ModifierUsage {
std::atomic<bool> checked{false}; // Extracted on at least one perimeter.
std::atomic<bool> reached{false}; // Gave a segment, full containment or a discarded fragment.
std::atomic<bool> checked{false}; // Extracted on at least one perimeter.
std::atomic<bool> reached{false}; // Gave a segment, full containment or a discarded fragment.
std::atomic<bool> fully_blocked{false}; // Seam Blocked whole zone in effect on a perimeter left Blocked.
};
std::unordered_map<const ModelVolume*, ModifierUsage> modifier_usage;
@@ -70,13 +72,18 @@ struct PreciseSeamWarnings {
assert(is_precise_seam(type));
return 1u << (int(type) - int(ModelVolumeType::PRECISE_SEAM_CENTER));
}
// Load before fetch_or: most calls find the bit already set, so shared cache lines stay clean.
// Load before writing: most calls find the bit or flag already set, so shared cache lines stay clean.
static void mark(std::atomic<unsigned> &mask, ModelVolumeType type)
{
const unsigned bit = type_bit(type);
if ((mask.load(std::memory_order_relaxed) & bit) == 0)
mask.fetch_or(bit, std::memory_order_relaxed);
}
static void set(std::atomic<bool> &flag)
{
if (!flag.load(std::memory_order_relaxed))
flag.store(true, std::memory_order_relaxed);
}
};
// Optional caller identity for concise diagnostics when an intersection is discarded.
@@ -139,9 +146,10 @@ struct WeakModifierSegment {
PerimeterPosition left_position; // Position on the source perimeter before insertion/refinement.
Point right_point; // Coordinates of right (last) point of segment
PerimeterPosition right_position; // Retained provenance, not an index into the modified polygon.
// Full containment of an Enforced or Neutral modifier: the zone is the whole perimeter, without
// boundaries (the points and positions above are unused and nothing is inserted for it).
// Full containment of a weak modifier: the zone is the whole perimeter, without boundaries
// (the points and positions above are unused and nothing is inserted for it).
bool whole_perimeter = false;
const ModelVolume *modifier = nullptr; // Source modifier, for the warnings.
};
// Collects the object's Precise Seam volumes: strong ones in priority order, weak ones in application
@@ -173,12 +181,14 @@ std::vector<WeakModifierSegment> collect_weak_modifier_segments(
PreciseSeamWarnings* warnings = nullptr);
// Retypes the candidates inside each zone in the given order (pass zones lowest priority first);
// sets some_point_enforced when an Enforced zone applies.
// sets some_point_enforced when an Enforced zone applies. When the whole perimeter ends up Blocked and
// the highest whole-perimeter zone is Blocked, flags that zone's modifier for the warning.
void apply_weak_modifiers_to_perimeter(
const std::vector<WeakModifierSegment> &weak_segments,
PrintObjectSeamData::LayerSeams &result,
const SeamPlacerImpl::Perimeter &perimeter,
bool &some_point_enforced);
bool &some_point_enforced,
PreciseSeamWarnings *warnings = nullptr);
// Restore precise seam positions that may have been modified by alignment
// Iterates through all perimeters and restores precise_seam_point positions
+41 -20
View File
@@ -608,7 +608,7 @@ void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, const
// Apply weak modifiers if no strong modifier was inserted
if (!inserted_seam_position.has_value() && !weak_segments.empty()) {
PreciseSeam::apply_weak_modifiers_to_perimeter(
weak_segments, result, perimeter, some_point_enforced);
weak_segments, result, perimeter, some_point_enforced, warnings);
}
if (some_point_enforced) {
@@ -896,18 +896,15 @@ struct SeamComparator {
return a.central_enforcer;
}
if (a.type == EnforcedBlockedSeamPoint::Enforced) {
return true;
}
if (a.type == EnforcedBlockedSeamPoint::Blocked) {
return false;
}
if (a.type != b.type) {
return a.type > b.type;
}
// Two Blocked points fall through like Neutral ones, so a perimeter blocked all round still gets a proper seam.
if (a.type == EnforcedBlockedSeamPoint::Enforced) {
return true;
}
//avoid overhangs
if ((a.overhang > 0.0f || b.overhang > 0.0f)
&& abs(a.overhang - b.overhang) > (0.1f * a.perimeter.flow_width)) {
@@ -1660,18 +1657,42 @@ void SeamPlacer::init(const Print &print, std::function<void(void)> throw_if_can
parts.push_back((boost::format(_u8L("multiple intersections with a perimeter, only one was used (%1%)")) % type_list(mi)).str());
if (fc != 0)
parts.push_back((boost::format(_u8L("a perimeter is fully inside a modifier, the modifier was not applied to it (%1%)")) % type_list(fc)).str());
// Modifiers evaluated somewhere that never reached a perimeter; never-evaluated ones are not reported.
// Print and volume order make the named one deterministic; the log lists them all.
std::vector<const ModelVolume*> no_effect;
for (const PrintObject *po : print.objects())
for (const ModelVolume *volume : po->model_object()->volumes) {
const auto it = precise_seam_warnings.modifier_usage.find(volume);
if (it != precise_seam_warnings.modifier_usage.end() &&
it->second.checked.load(std::memory_order_relaxed) &&
!it->second.reached.load(std::memory_order_relaxed) &&
std::find(no_effect.begin(), no_effect.end(), volume) == no_effect.end())
no_effect.push_back(volume);
// Modifiers whose usage flags match, in print and volume order, so the named one is deterministic.
const auto collect_modifiers = [&print, &precise_seam_warnings](const auto &matches) {
std::vector<const ModelVolume*> found;
for (const PrintObject *po : print.objects())
for (const ModelVolume *volume : po->model_object()->volumes) {
const auto it = precise_seam_warnings.modifier_usage.find(volume);
if (it != precise_seam_warnings.modifier_usage.end() && matches(it->second) &&
std::find(found.begin(), found.end(), volume) == found.end())
found.push_back(volume);
}
return found;
};
// Seam Blocked modifiers whose whole perimeter stayed Blocked; the user warning names only the
// first one, the log lists them all.
const std::vector<const ModelVolume*> fully_blocked = collect_modifiers(
[](const PreciseSeam::PreciseSeamWarnings::ModifierUsage &usage) { return usage.fully_blocked.load(std::memory_order_relaxed); });
for (const ModelVolume *volume : fully_blocked)
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamFullyBlocked] object=\"" << volume->get_object()->name
<< "\" modifier=\"" << volume->name << "\"";
if (!fully_blocked.empty()) {
const ModelVolume *first = fully_blocked.front();
if (fully_blocked.size() == 1) {
// TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name.
parts.push_back((boost::format(_u8L("%1% modifier \"%2%\" of \"%3%\" covers an entire perimeter"))
% _u8L("Seam Blocked") % first->name % first->get_object()->name).str());
} else {
// TRN: %1% is the modifier type "Seam Blocked" as in the menu, %2% the modifier name, %3% the object name, %4% the number of modifiers.
parts.push_back((boost::format(_u8L("%1% modifier \"%2%\" of \"%3%\" (%4% in total) covers an entire perimeter"))
% _u8L("Seam Blocked") % first->name % first->get_object()->name % fully_blocked.size()).str());
}
}
// Modifiers evaluated somewhere that never reached a perimeter; never-evaluated ones are not reported.
const std::vector<const ModelVolume*> no_effect = collect_modifiers(
[](const PreciseSeam::PreciseSeamWarnings::ModifierUsage &usage) {
return usage.checked.load(std::memory_order_relaxed) && !usage.reached.load(std::memory_order_relaxed);
});
// The user warning names only the first one; the log lists them all.
for (const ModelVolume *volume : no_effect)
BOOST_LOG_TRIVIAL(warning) << "[PreciseSeamNoEffect] object=\"" << volume->get_object()->name
@@ -20,6 +20,8 @@
#include "libslic3r/Exception.hpp"
#include "libslic3r/GCode/PchipInterpolatorHelper.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/format.hpp"
#include "SmallAreaInfillFlowCompensator.hpp"
#include <boost/log/trivial.hpp>
@@ -55,10 +57,8 @@ SmallAreaInfillFlowCompensator::SmallAreaInfillFlowCompensator(const Slic3r::GCo
flowComps.push_back(std::stod(value_str));
}
} catch (...) {
std::stringstream ss;
ss << "Small Area Flow Compensation: Error parsing data point in small area infill compensation model:" << line << std::endl;
throw Slic3r::InvalidArgument(ss.str());
// TRN %1% is the line of the model that could not be parsed.
throw Slic3r::InvalidArgument(Slic3r::format(_u8L("Small Area Flow Compensation: Error parsing data point in small area infill compensation model: %1%"), line) + "\n");
}
}
}
@@ -66,26 +66,26 @@ SmallAreaInfillFlowCompensator::SmallAreaInfillFlowCompensator(const Slic3r::GCo
for (size_t i = 0; i < eLengths.size(); i++) {
if (i == 0) {
if (!nearly_equal(eLengths[i], 0.0)) {
throw Slic3r::InvalidArgument("Small Area Flow Compensation: First extrusion length for small area infill compensation model must be 0");
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: First extrusion length for small area infill compensation model must be 0"));
}
} else {
if (nearly_equal(eLengths[i], 0.0)) {
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Only the first extrusion length for small area infill compensation model can be 0");
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Only the first extrusion length for small area infill compensation model can be 0"));
}
if (eLengths[i] <= eLengths[i - 1]) {
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Extrusion lengths for subsequent points must be increasing");
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Extrusion lengths for subsequent points must be increasing"));
}
}
}
for (size_t i = 1; i < flowComps.size(); ++i) {
if (flowComps[i] <= flowComps[i - 1]) {
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Flow compensation factors must strictly increase with extrusion length");
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Flow compensation factors must strictly increase with extrusion length"));
}
}
if (!flowComps.empty() && !nearly_equal(flowComps.back(), 1.0)) {
throw Slic3r::InvalidArgument("Small Area Flow Compensation: Final compensation factor for small area infill flow compensation model must be 1.0");
throw Slic3r::InvalidArgument(_u8L("Small Area Flow Compensation: Final compensation factor for small area infill flow compensation model must be 1.0"));
}
flowModel = std::make_unique<PchipInterpolatorHelper>(eLengths, flowComps);
+4 -3
View File
@@ -5,6 +5,7 @@
#include "libslic3r/Config.hpp"
#include "libslic3r/enum_bitmask.hpp"
#include "format.hpp"
#include "libslic3r/I18N.hpp"
#include <boost/algorithm/string/case_conv.hpp>
#include <qoi/qoi.h>
@@ -610,11 +611,11 @@ std::string get_error_string(const ThumbnailErrors& errors)
std::string error_str;
if (errors.has(ThumbnailError::InvalidVal))
error_str += "\n - " + Slic3r::format("Invalid input format. Expected vector of dimensions in the following format: \"%1%\"", "XxY/EXT, XxY/EXT, ...");
error_str += "\n - " + Slic3r::format(_u8L("Invalid input format. Expected vector of dimensions in the following format: \"%1%\""), "XxY/EXT, XxY/EXT, ...");
if (errors.has(ThumbnailError::OutOfRange))
error_str += "\n - Input value is out of range";
error_str += "\n - " + _u8L("Input value is out of range");
if (errors.has(ThumbnailError::InvalidExt))
error_str += "\n - Some extension in the input is invalid";
error_str += "\n - " + _u8L("Some extension in the input is invalid");
return error_str;
}
+1 -1
View File
@@ -1646,7 +1646,7 @@ static FilamentGroupContext build_filament_group_context(
context.nozzle_info.extruder_nozzle_list = build_extruder_nozzle_list(context.nozzle_info.nozzle_list);
if (context.nozzle_info.nozzle_list.empty())
throw Slic3r::RuntimeError("No valid nozzle found. Please check nozzle count.");
throw Slic3r::RuntimeError(_L("No valid nozzle found. Please check nozzle count."));
if (!nozzle_status.empty())
context.nozzle_info.nozzle_status = nozzle_status;
+11 -1
View File
@@ -4,6 +4,7 @@
#include "WipeTower.hpp"
#include "WipeTower2.hpp"
#include "../Config.hpp"
#include "../FilamentMixer.hpp"
#include "../PrintConfig.hpp"
#include "../libslic3r.h"
@@ -74,10 +75,19 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo
return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.);
}
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_ids, double layer_height, double max_object_height)
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_slots, double layer_height, double max_object_height)
{
WipeTowerFootprint footprint;
footprint.height = max_object_height;
// A mixed slot is virtual: the tool ordering resolves it to one of its physical components
// on every layer, with or without colour sublayers, so the tower only ever purges those.
// Validation passes the slots as Print::extruders() lists them; the GUI and CLI placement
// expand them beforehand, which repeating here leaves unchanged.
std::vector<unsigned int> filament_ids = filament_slots;
const auto *is_mixed_opt = dynamic_cast<const ConfigOptionBools *>(option_of(config, "filament_is_mixed"));
const auto *components_opt = dynamic_cast<const ConfigOptionStrings *>(option_of(config, "filament_mixed_components"));
if (is_mixed_opt != nullptr && components_opt != nullptr && has_any_mixed_filament(is_mixed_opt->values))
filament_ids = expand_mixed_filaments(filament_slots, is_mixed_opt->values, components_opt->values);
const size_t filaments_cnt = filament_ids.size();
if (filaments_cnt == 0 || layer_height < EPSILON)
return footprint;
@@ -34,6 +34,8 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo
// filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool
// changes, so ids derived from the model must include them
// (Print::extruders(true)) or a real tower is sized as if it were never built.
// A mixed slot may be passed as itself or already expanded: it is sized as its
// physical components either way, since those are all the tower ever purges.
// layer_height: thinnest layer the objects are sliced at. The first layer is folded in here.
//
// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate
+4 -2
View File
@@ -198,10 +198,12 @@ std::string imex_multicolor_block_reason(const std::string& parallel_mode,
// Filaments are numbered from 1 as the sidebar shows them, heads by their T index; the map
// decides which head a filament is on, so with MMU lanes the two numbers differ.
if (phys < 0)
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% isn't routed to any "
// TRN %1% is a list of tools such as "T0/T1", %2% a filament number.
return (boost::format(L("In this Span mode, colors are printed only by %1%, but filament %2% isn't routed to any "
"tool. Use filaments routed to %1%, or switch this plate to Primary mode.")) %
tools % (filament + 1)).str();
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% is routed to T%3%. Use "
// TRN %1% is a list of tools such as "T0/T1", %2% a filament number, %3% a tool number.
return (boost::format(L("In this Span mode, colors are printed only by %1%, but filament %2% is routed to T%3%. Use "
"filaments routed to %1%, or switch this plate to Primary mode.")) %
tools % (filament + 1) % phys).str();
}
+30 -9
View File
@@ -354,6 +354,24 @@ Model Model::read_from_file(const std::string&
ObjInfo obj_info;
ObjParser::MtlData mtl_data;
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
// Keep the model's own UV channel on the volume, whatever the colour import then does with
// the mesh: it is what TextureProjectionMethod::ModelUV maps a texture-displacement layer
// through, so a model that arrives with a UV layout is textured on that layout instead of on
// a fresh unwrap. load_obj() fills obj_info.uvs per face of the mesh it built, in the same
// order (and only when every face named a known material, hence the size check). V is flipped
// on the way in, to the convention DecodedHeightTexture samples in - the same flip
// obj_to_textured_mesh() does for the colour importer.
if (result && !obj_info.uvs.empty() && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
ModelVolume &volume = *model.objects.back()->volumes[0];
const indexed_triangle_set &its = volume.mesh().its;
if (obj_info.uvs.size() == its.indices.size()) {
volume.texture_uv.resize(its.indices.size() * 3);
for (size_t fi = 0; fi < obj_info.uvs.size(); ++fi)
for (int k = 0; k < 3; ++k)
volume.texture_uv[fi * 3 + size_t(k)] =
Vec2f(obj_info.uvs[fi][size_t(k)].x(), 1.f - obj_info.uvs[fi][size_t(k)].y());
}
}
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
// Textured OBJ: hand the mesh + materials to the texture-to-color importer
// instead of the flat per-face colour dialog.
@@ -454,7 +472,7 @@ Model Model::read_from_file(const std::string&
boost::algorithm::iends_with(input_file, ".abc") || boost::algorithm::iends_with(input_file, ".ply")) {
std::string temp_stl = make_temp_stl_with_modelio(input_file);
if (temp_stl.empty()) {
throw Slic3r::RuntimeError("Failed to convert asset to STL via ModelIO.");
throw Slic3r::RuntimeError(_L("Failed to convert asset to STL via ModelIO."));
}
result = load_stl(temp_stl.c_str(), &model);
delete_temp_file(temp_stl);
@@ -1262,6 +1280,7 @@ ModelObject& ModelObject::assign_copy(const ModelObject &rhs)
this->layer_config_ranges = rhs.layer_config_ranges;
this->layer_height_profile = rhs.layer_height_profile;
this->printable = rhs.printable;
this->auto_drop = rhs.auto_drop;
this->origin_translation = rhs.origin_translation;
this->cut_id.copy(rhs.cut_id);
this->copy_transformation_caches(rhs);
@@ -1302,6 +1321,7 @@ ModelObject& ModelObject::assign_copy(ModelObject &&rhs)
this->layer_config_ranges = std::move(rhs.layer_config_ranges);
this->layer_height_profile = std::move(rhs.layer_height_profile);
this->printable = std::move(rhs.printable);
this->auto_drop = rhs.auto_drop;
this->origin_translation = std::move(rhs.origin_translation);
this->copy_transformation_caches(rhs);
@@ -1871,6 +1891,9 @@ void ModelObject::center_around_origin(bool include_modifiers)
void ModelObject::ensure_on_bed(bool allow_negative_z)
{
if (!auto_drop)
return;
double z_offset = 0.0;
if (allow_negative_z) {
@@ -1889,14 +1912,8 @@ void ModelObject::ensure_on_bed(bool allow_negative_z)
else
z_offset = -this->min_z();
if (z_offset != 0.0) {
for (size_t i = 0; i < instances.size(); ++i) {
if (!instances[i]->auto_drop)
continue;
translate_instance(i, z_offset * Vec3d::UnitZ());
}
}
if (z_offset != 0.0)
translate_instances(z_offset * Vec3d::UnitZ());
}
void ModelObject::translate_instances(const Vec3d& vector)
@@ -2134,6 +2151,10 @@ void ModelVolume::reset_extra_facets()
// than left referring to a mesh that no longer matches it.
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).reset();
// Same reasoning for the model's own UV channel: it is indexed by triangle, so against a mesh
// this no longer matches it would map the texture by another mesh's triangle numbering. A caller
// that can carry it across - see transfer_uv_channel() - puts it back after this.
this->reset_texture_uv();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const
+30 -8
View File
@@ -400,6 +400,9 @@ public:
LayerHeightProfile layer_height_profile;
// Whether or not this object is printable
bool printable { true };
// Whether this object is snapped to the build plate after transformations.
// Instance Z offsets are shared by all instances of an object.
bool auto_drop { true };
// This vector holds position of selected support points for SLA. The data are
// saved in mesh coordinates to allow using them for several instances.
@@ -706,7 +709,7 @@ private:
Internal::StaticSerializationWrapper<ModelConfigObject const> config_wrapper(config);
Internal::StaticSerializationWrapper<LayerHeightProfile const> layer_heigth_profile_wrapper(layer_height_profile);
ar(name, module_name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper,
sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, brim_points,
sla_support_points, sla_points_status, sla_drain_holes, printable, auto_drop, origin_translation, brim_points,
m_bounding_box_approx, m_bounding_box_approx_valid,
m_bounding_box_exact, m_bounding_box_exact_valid, m_min_max_z_valid,
m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid,
@@ -719,7 +722,7 @@ private:
// BBS: add backup, check modify
SaveObjectGaurd gaurd(*this);
ar(name, module_name, input_file, instances, volumes, config_wrapper, layer_config_ranges, layer_heigth_profile_wrapper,
sla_support_points, sla_points_status, sla_drain_holes, printable, origin_translation, brim_points,
sla_support_points, sla_points_status, sla_drain_holes, printable, auto_drop, origin_translation, brim_points,
m_bounding_box_approx, m_bounding_box_approx_valid,
m_bounding_box_exact, m_bounding_box_exact_valid, m_min_max_z_valid,
m_raw_bounding_box, m_raw_bounding_box_valid, m_raw_mesh_bounding_box, m_raw_mesh_bounding_box_valid,
@@ -975,6 +978,26 @@ public:
// they are not a property of any single layer.
TextureDisplacementOptions texture_displacement_options;
// The model's own texture coordinates, as the file it was imported from carried them: three per
// triangle of mesh().its, in its triangle order (corner k of triangle i at 3i+k), V already
// flipped to the sampling convention (V = 0 is the image's top row). Empty when the source had
// none, which is every STL and every 3mf - only a textured OBJ brings one in today.
//
// Per corner rather than per vertex because that is what a UV layout is: the two sides of a seam
// are the same vertex with two different coordinates, and collapsing them would smear the texture
// across every island boundary. TextureProjectionMethod::ModelUV maps through it, which is what
// lets a model that already carries a UV channel be textured on the artist's own layout instead
// of on a fresh unwrap.
//
// Indexed by triangle, so anything that *replaces* the mesh has to bring it across (or drop it):
// see transfer_uv_channel(), and reset_texture_uv() for the cases that cannot.
std::vector<Vec2f> texture_uv;
bool has_texture_uv() const { return !texture_uv.empty() && texture_uv.size() == mesh().its.indices.size() * 3; }
// Forgets the channel. For a mesh change that nothing can carry it across - a split, a boolean -
// where leaving it would map the texture by another mesh's triangle numbering.
void reset_texture_uv() { texture_uv.clear(); }
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1240,6 +1263,7 @@ private:
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
texture_displacement_layers(other.texture_displacement_layers),
texture_displacement_options(other.texture_displacement_options),
texture_uv(other.texture_uv),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{
assert(this->id().valid());
@@ -1338,7 +1362,7 @@ private:
cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp();
}
ar(texture_displacement_layers, texture_displacement_options);
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1362,7 +1386,7 @@ private:
cereal::save_by_value(ar, fuzzy_skin_facets);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
cereal::save_by_value(ar, texture_displacement_facet(i));
ar(texture_displacement_layers, texture_displacement_options);
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);
@@ -1406,7 +1430,6 @@ public:
ModelInstanceEPrintVolumeState print_volume_state;
// Whether or not this instance is printable
bool printable;
bool auto_drop;
bool use_loaded_id_for_label {false};
int arrange_order = 0; // BBS
size_t loaded_id = 0; // BBS
@@ -1536,7 +1559,7 @@ private:
// Constructor, which assigns a new unique ID.
explicit ModelInstance(ModelObject* object)
: print_volume_state(ModelInstancePVS_Inside), printable(true), auto_drop(true), object(object), m_assemble_initialized(false)
: print_volume_state(ModelInstancePVS_Inside), printable(true), object(object), m_assemble_initialized(false)
{
assert(this->id().valid());
}
@@ -1547,7 +1570,6 @@ private:
, m_offset_to_assembly(other.m_offset_to_assembly)
, print_volume_state(ModelInstancePVS_Inside)
, printable(other.printable)
, auto_drop(other.auto_drop)
, object(object)
, m_assemble_initialized(false) { assert(this->id().valid() && this->id() != other.id()); }
@@ -1561,7 +1583,7 @@ private:
ModelInstance() : ObjectBase(-1), object(nullptr) { assert(this->id().invalid()); }
// BBS. Add added members to archive.
template<class Archive> void serialize(Archive& ar) {
ar(m_transformation, print_volume_state, printable, auto_drop, m_assemble_transformation, m_offset_to_assembly, m_assemble_initialized);
ar(m_transformation, print_volume_state, printable, m_assemble_transformation, m_offset_to_assembly, m_assemble_initialized);
}
};
@@ -1196,7 +1196,6 @@ static void cut_segmented_layers(const std::vector<ExPolygons> &input_exp
const std::function<void()> &throw_on_cancel_callback)
{
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - cutting segmented layers in parallel - begin";
const float interlocking_cut_width = interlocking_depth > 0.f ? std::max(cut_width - interlocking_depth, 0.f) : 0.f;
tbb::parallel_for(tbb::blocked_range<size_t>(0, segmented_regions.size()),
[&segmented_regions, &input_expolygons, &cut_width, &interlocking_depth, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
+133 -132
View File
@@ -5,6 +5,8 @@
#include "PrintConfig.hpp"
#include "Point.hpp"
#include "Utils.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "libslic3r_version.h"
#include <cassert>
#include <boost/range/iterator_range_core.hpp>
@@ -383,7 +385,7 @@ namespace client
case TYPE_DOUBLE:
return expr(- this->d(), start_pos, this->it_range.end());
default:
this->throw_exception("Cannot apply unary minus operator.");
this->throw_exception(L("Cannot apply unary minus operator."));
}
assert(false);
// Suppress compiler warnings.
@@ -401,7 +403,7 @@ namespace client
case TYPE_DOUBLE:
return expr(static_cast<int>(this->d()), start_pos, this->it_range.end());
default:
this->throw_exception("Cannot convert to integer.");
this->throw_exception(L("Cannot convert to integer."));
}
assert(false);
// Suppress compiler warnings.
@@ -419,7 +421,7 @@ namespace client
case TYPE_DOUBLE:
return expr(static_cast<int>(std::round(this->d())), start_pos, this->it_range.end());
default:
this->throw_exception("Cannot round a non-numeric value.");
this->throw_exception(L("Cannot round a non-numeric value."));
}
assert(false);
// Suppress compiler warnings.
@@ -434,7 +436,7 @@ namespace client
case TYPE_DOUBLE:
return expr(static_cast<int>(std::floor(this->d())), start_pos, this->it_range.end());
default:
this->throw_exception("Cannot floor a non-numeric value.");
this->throw_exception(L("Cannot floor a non-numeric value."));
}
assert(false);
// Suppress compiler warnings.
@@ -449,7 +451,7 @@ namespace client
case TYPE_DOUBLE:
return expr(static_cast<int>(std::ceil(this->d())), start_pos, this->it_range.end());
default:
this->throw_exception("Cannot ceil a non-numeric value.");
this->throw_exception(L("Cannot ceil a non-numeric value."));
}
assert(false);
// Suppress compiler warnings.
@@ -465,7 +467,7 @@ namespace client
case TYPE_BOOL:
return expr(! this->b(), start_pos, this->it_range.end());
default:
this->throw_exception("Cannot apply a not operator.");
this->throw_exception(L("Cannot apply a not operator."));
}
assert(false);
// Suppress compiler warnings.
@@ -483,7 +485,7 @@ namespace client
// Conver the left hand side to string, append rhs.
this->set_s(this->to_string() + rhs.s());
} else {
const char *err_msg = "Cannot add non-numeric types.";
const char *err_msg = L("Cannot add non-numeric types.");
this->throw_if_not_numeric(err_msg);
rhs.throw_if_not_numeric(err_msg);
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
@@ -501,7 +503,7 @@ namespace client
// Inside an if / else block to be skipped.
this->reset();
} else {
const char *err_msg = "Cannot subtract non-numeric types.";
const char *err_msg = L("Cannot subtract non-numeric types.");
this->throw_if_not_numeric(err_msg);
rhs.throw_if_not_numeric(err_msg);
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
@@ -519,7 +521,7 @@ namespace client
// Inside an if / else block to be skipped.
this->reset();
} else {
const char *err_msg = "Cannot multiply with non-numeric type.";
const char *err_msg = L("Cannot multiply with non-numeric type.");
this->throw_if_not_numeric(err_msg);
rhs.throw_if_not_numeric(err_msg);
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
@@ -537,10 +539,10 @@ namespace client
// Inside an if / else block to be skipped.
this->reset();
} else {
this->throw_if_not_numeric("Cannot divide a non-numeric type.");
rhs.throw_if_not_numeric("Cannot divide with a non-numeric type.");
this->throw_if_not_numeric(L("Cannot divide a non-numeric type."));
rhs.throw_if_not_numeric(L("Cannot divide with a non-numeric type."));
if (rhs.type() == TYPE_INT ? (rhs.i() == 0) : (rhs.d() == 0.))
rhs.throw_exception("Division by zero");
rhs.throw_exception(L("Division by zero"));
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
this->set_d_lite(this->as_d() / rhs.as_d());
else
@@ -556,10 +558,10 @@ namespace client
// Inside an if / else block to be skipped.
this->reset();
} else {
this->throw_if_not_numeric("Cannot divide a non-numeric type.");
rhs.throw_if_not_numeric("Cannot divide with a non-numeric type.");
this->throw_if_not_numeric(L("Cannot divide a non-numeric type."));
rhs.throw_if_not_numeric(L("Cannot divide with a non-numeric type."));
if (rhs.type() == TYPE_INT ? (rhs.i() == 0) : (rhs.d() == 0.))
rhs.throw_exception("Division by zero");
rhs.throw_exception(L("Division by zero"));
if (this->type() == TYPE_DOUBLE || rhs.type() == TYPE_DOUBLE)
this->set_d_lite(std::fmod(this->as_d(), rhs.as_d()));
else
@@ -581,7 +583,7 @@ namespace client
if (self.type() != TYPE_EMPTY) {
// Not inside an if / else block to be skipped
if (self.type() != TYPE_BOOL)
self.throw_exception("Not a boolean expression");
self.throw_exception(L("Not a boolean expression"));
out = self.b();
}
}
@@ -590,7 +592,7 @@ namespace client
{
assert(self.type() != TYPE_EMPTY);
if (self.type() != TYPE_BOOL)
self.throw_exception("Not a boolean expression");
self.throw_exception(L("Not a boolean expression"));
out = self.b() ? "true" : "false";
}
@@ -622,7 +624,7 @@ namespace client
// Both type are bool.
if (op != '=')
boost::throw_exception(qi::expectation_failure<Iterator>(
lhs.it_range.begin(), rhs.it_range.end(), spirit::info("*Cannot compare the types.")));
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot compare the types."))));
value = lhs.b() == rhs.b();
} else if (lhs.type() == TYPE_STRING || rhs.type() == TYPE_STRING) {
// One type is string, the other could be converted to string.
@@ -630,7 +632,7 @@ namespace client
(op == '<') ? (lhs.to_string() < rhs.to_string()) : (lhs.to_string() > rhs.to_string());
} else {
boost::throw_exception(qi::expectation_failure<Iterator>(
lhs.it_range.begin(), rhs.it_range.end(), spirit::info("*Cannot compare the types.")));
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot compare the types."))));
}
lhs.reset();
lhs.set_b_lite(invert ? ! value : value);
@@ -645,7 +647,7 @@ namespace client
static void throw_if_not_numeric(const expr &param)
{
const char *err_msg = "Not a numeric type.";
const char *err_msg = L("Not a numeric type.");
param.throw_if_not_numeric(err_msg);
}
@@ -707,10 +709,10 @@ namespace client
return;
throw_if_not_numeric(param1);
if (param2.type() != TYPE_INT)
param2.throw_exception("digits: second parameter must be integer");
param2.throw_exception(L("digits: second parameter must be integer"));
bool has_decimals = param3.type() != TYPE_EMPTY;
if (has_decimals && param3.type() != TYPE_INT)
param3.throw_exception("digits: third parameter must be integer");
param3.throw_exception(L("digits: third parameter must be integer"));
char buf[256];
int ndigits = std::clamp(param2.as_i(), 0, 64);
@@ -734,7 +736,7 @@ namespace client
// One type is string, the other could be converted to string.
subject = &lhs.s();
} else {
lhs.throw_exception("Left hand side of a regex match must be a string.");
lhs.throw_exception(L("Left hand side of a regex match must be a string."));
}
try {
std::string pattern(++ rhs.begin(), -- rhs.end());
@@ -745,7 +747,7 @@ namespace client
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
// Syntax error in the regular expression
boost::throw_exception(qi::expectation_failure<Iterator>(
rhs.begin(), rhs.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what())));
rhs.begin(), rhs.end(), spirit::info("*" + Slic3r::format(_u8L("Regular expression compilation failed: %1%"), ex.what()))));
}
}
@@ -760,7 +762,7 @@ namespace client
// Inside an if / else block to be skipped
return;
if (subject.type() != TYPE_STRING)
subject.throw_exception("regex_replace() first parameter must be a string.");
subject.throw_exception(L("regex_replace() first parameter must be a string."));
try {
std::string re(++ pattern.begin(), -- pattern.end());
std::string result = SLIC3R_REGEX_NAMESPACE::regex_replace(subject.s(), SLIC3R_REGEX_NAMESPACE::regex(re), replacement.to_string());
@@ -768,7 +770,7 @@ namespace client
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
// Syntax error in the regular expression
boost::throw_exception(qi::expectation_failure<Iterator>(
pattern.begin(), pattern.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what())));
pattern.begin(), pattern.end(), spirit::info("*" + Slic3r::format(_u8L("Regular expression compilation failed: %1%"), ex.what()))));
}
}
@@ -784,15 +786,15 @@ namespace client
}
if (! out.b()) {
if (match.type() != TYPE_STRING)
match.throw_exception("one_of(): First parameter (the string to match against) has to be a string value");
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
if (pattern.type() != TYPE_STRING)
match.throw_exception("one_of(): Pattern has to be a string value");
match.throw_exception(L("one_of(): Pattern has to be a string value"));
if (RegEx) {
try {
out.set_b(SLIC3R_REGEX_NAMESPACE::regex_match(match.s(), SLIC3R_REGEX_NAMESPACE::regex(pattern.s())));
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &) {
// Syntax error in the regular expression
pattern.throw_exception("Regular expression compilation failed");
pattern.throw_exception(L("Regular expression compilation failed"));
}
} else
out.set_b(match.s() == pattern.s());
@@ -806,7 +808,7 @@ namespace client
}
if (! out.b()) {
if (match.type() != TYPE_STRING)
match.throw_exception("one_of(): First parameter (the string to match against) has to be a string value");
match.throw_exception(L("one_of(): First parameter (the string to match against) has to be a string value"));
regex_op(match, pattern, '=', out);
}
}
@@ -821,7 +823,7 @@ namespace client
value = (op == '|') ? (lhs.b() || rhs.b()) : (lhs.b() && rhs.b());
} else {
boost::throw_exception(qi::expectation_failure<Iterator>(
lhs.it_range.begin(), rhs.it_range.end(), spirit::info("*Cannot apply logical operation to non-boolean operators.")));
lhs.it_range.begin(), rhs.it_range.end(), spirit::info(std::string("*") + L("Cannot apply logical operation to non-boolean operands."))));
}
lhs.set_b_lite(value);
}
@@ -984,30 +986,30 @@ namespace client
opt = ctx->resolve_symbol(opt_key_str.substr(0, idx));
if (opt != nullptr) {
if (! opt->is_vector())
ctx->throw_exception("Trying to index a scalar variable", opt_key);
ctx->throw_exception(L("Trying to index a scalar variable"), opt_key);
char *endptr = nullptr;
idx = strtol(opt_key_str.c_str() + idx + 1, &endptr, 10);
if (endptr == nullptr || *endptr != 0)
ctx->throw_exception("Invalid vector index", IteratorRange(opt_key.begin() + idx + 1, opt_key.end()));
ctx->throw_exception(L("Invalid vector index"), IteratorRange(opt_key.begin() + idx + 1, opt_key.end()));
}
}
}
if (opt == nullptr)
ctx->throw_exception(ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist", opt_key);
ctx->throw_exception(ctx->skipping() ? L("Variable does not exist (in an inactive branch)") : L("Variable does not exist"), opt_key);
if (ctx->skipping())
return;
if (opt->is_scalar()) {
if (opt->is_nil())
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
ctx->throw_exception(L("Trying to reference an undefined (nil) optional variable"), opt_key);
output = opt->serialize();
} else {
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", opt_key);
ctx->throw_exception(L("Indexing an empty vector variable"), opt_key);
if (idx >= vec->size())
idx = 0;
if (vec->is_nil(idx))
ctx->throw_exception("Trying to reference an undefined (nil) element of vector of optional values", opt_key);
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt_key);
output = vec->vserialize()[idx];
}
}
@@ -1021,7 +1023,7 @@ namespace client
if (ctx->skipping() && ! ctx->check_inactive_names())
return;
const char *not_found = ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist";
const char *not_found = ctx->skipping() ? L("Variable does not exist (in an inactive branch)") : L("Variable does not exist");
std::string opt_key_str(opt_key.begin(), opt_key.end());
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
if (opt == nullptr) {
@@ -1034,24 +1036,24 @@ namespace client
ctx->throw_exception(not_found, opt_key);
}
if (! opt->is_vector())
ctx->throw_exception("Trying to index a scalar variable", opt_key);
ctx->throw_exception(L("Trying to index a scalar variable"), opt_key);
const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end()));
if (opt_index == nullptr)
ctx->throw_exception(not_found, opt_key);
if (opt_index->type() != coInt)
ctx->throw_exception("Indexing variable has to be integer", opt_key);
ctx->throw_exception(L("Indexing variable has to be integer"), opt_key);
if (ctx->skipping())
return;
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", opt_key);
ctx->throw_exception(L("Indexing an empty vector variable"), opt_key);
int idx = opt_index->getInt();
if (idx < 0)
ctx->throw_exception("Negative vector index", opt_key);
ctx->throw_exception(L("Negative vector index"), opt_key);
if (idx >= (int)vec->size())
idx = 0;
if (vec->is_nil(idx))
ctx->throw_exception("Trying to reference an undefined (nil) element of vector of optional values", opt_key);
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt_key);
output = vec->vserialize()[idx];
}
@@ -1066,7 +1068,7 @@ namespace client
if (opt == nullptr) {
opt = ctx->resolve_output_symbol(key);
if (opt == nullptr)
ctx->throw_exception("Not a variable name", opt_key);
ctx->throw_exception(L("Not a variable name"), opt_key);
output.writable = true;
}
output.opt = opt;
@@ -1076,7 +1078,7 @@ namespace client
if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr &&
ctx->inactive_local_variables.count(key) == 0 &&
(ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0))
ctx->throw_exception("Not a variable name (in an inactive branch)", opt_key);
ctx->throw_exception(L("Not a variable name (in an inactive branch)"), opt_key);
}
output.it_range = opt_key;
}
@@ -1110,7 +1112,7 @@ namespace client
assert(opt.opt->is_scalar());
if (opt.opt->is_nil())
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt.it_range);
ctx->throw_exception(L("Trying to reference an undefined (nil) optional variable"), opt.it_range);
switch (opt.opt->type()) {
case coFloat: output.set_d(opt.opt->getFloat()); break;
@@ -1137,7 +1139,7 @@ namespace client
for (;;) {
const ConfigOption *opt_parent = opt_def->ratio_over.empty() ? nullptr : ctx->resolve_symbol(opt_def->ratio_over);
if (opt_parent == nullptr)
ctx->throw_exception("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies", opt.it_range);
ctx->throw_exception(L("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies"), opt.it_range);
if (boost::ends_with(opt_def->ratio_over, "line_width")) {
// Line width supports defaults and a complex graph of dependencies.
assert(opt_parent->type() == coFloatOrPercent);
@@ -1159,7 +1161,7 @@ namespace client
break;
}
default:
ctx->throw_exception("Unsupported scalar variable type", opt.it_range);
ctx->throw_exception(L("Unsupported scalar variable type"), opt.it_range);
}
}
@@ -1173,7 +1175,7 @@ namespace client
assert(opt.opt->is_vector());
const ConfigOptionVectorBase* vec = static_cast<const ConfigOptionVectorBase*>(opt.opt);
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", opt.it_range);
ctx->throw_exception(L("Indexing an empty vector variable"), opt.it_range);
// Helper to resolve a FloatOrPercent value (handles ratio_over chain for percent values).
// elem_index: the element index used to access this vector element, so that
@@ -1194,7 +1196,7 @@ namespace client
for (;;) {
const ConfigOption *opt_parent = opt_def->ratio_over.empty() ? nullptr : ctx->resolve_symbol(opt_def->ratio_over);
if (opt_parent == nullptr)
ctx->throw_exception("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies", opt.it_range);
ctx->throw_exception(L("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies"), opt.it_range);
if (boost::ends_with(opt_def->ratio_over, "line_width")) {
// Line width supports defaults and a complex graph of dependencies.
assert(opt_parent->type() == coFloatOrPercent);
@@ -1277,12 +1279,12 @@ namespace client
}
break;
}
default: ctx->throw_exception("Referencing a vector variable when scalar is expected", opt.it_range);
default: ctx->throw_exception(L("Referencing a vector variable when scalar is expected"), opt.it_range);
}
} else {
size_t idx = (opt.index < 0) ? 0 : (opt.index >= int(vec->size())) ? 0 : size_t(opt.index);
if (vec->is_nil(idx))
ctx->throw_exception("Trying to reference an undefined (nil) element of vector of optional values", opt.it_range);
ctx->throw_exception(L("Trying to reference an undefined (nil) element of vector of optional values"), opt.it_range);
switch (opt.opt->type()) {
case coFloats: output.set_d(static_cast<const ConfigOptionFloats*>(opt.opt)->values[idx]); break;
case coInts: output.set_i(static_cast<const ConfigOptionInts*>(opt.opt)->values[idx]); break;
@@ -1301,27 +1303,27 @@ namespace client
break;
}
default:
ctx->throw_exception("Unsupported vector variable type", opt.it_range);
ctx->throw_exception(L("Unsupported vector variable type"), opt.it_range);
}
}
}
static void check_writable(const MyContext *ctx, OptWithPos &opt) {
if (! opt.writable)
ctx->throw_exception("Cannot modify a read-only variable", opt.it_range);
ctx->throw_exception(L("Cannot modify a read-only variable"), opt.it_range);
}
static void check_numeric(const expr &param) {
if (! param.numeric_type())
param.throw_exception("Right side is not a numeric expression");
param.throw_exception(L("Right side is not a numeric expression"));
};
static size_t evaluate_count(const expr &expr_count) {
if (expr_count.type() != expr::TYPE_INT)
expr_count.throw_exception("Expected number of elements to fill a vector with.");
expr_count.throw_exception(L("Expected number of elements to fill a vector with."));
int count = expr_count.i();
if (count < 0)
expr_count.throw_exception("Negative number of elements specified.");
expr_count.throw_exception(L("Negative number of elements specified."));
return size_t(count);
};
@@ -1349,11 +1351,11 @@ namespace client
break;
case coBool:
if (rhs.type() != expr::TYPE_BOOL)
ctx->throw_exception("Right side is not a boolean expression", rhs.it_range);
ctx->throw_exception(L("Right side is not a boolean expression"), rhs.it_range);
static_cast<ConfigOptionBool*>(wropt)->value = rhs.b();
break;
default:
ctx->throw_exception("Unsupported output scalar variable type", lhs.it_range);
ctx->throw_exception(L("Unsupported output scalar variable type"), lhs.it_range);
}
}
@@ -1363,12 +1365,12 @@ namespace client
assert(lhs.opt->is_vector());
check_writable(ctx, lhs);
if (! lhs.has_index())
ctx->throw_exception("Referencing an output vector variable when scalar is expected", lhs.it_range);
ctx->throw_exception(L("Referencing an output vector variable when scalar is expected"), lhs.it_range);
ConfigOptionVectorBase *vec = const_cast<ConfigOptionVectorBase*>(static_cast<const ConfigOptionVectorBase*>(lhs.opt));
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", lhs.it_range);
ctx->throw_exception(L("Indexing an empty vector variable"), lhs.it_range);
if (lhs.index >= int(vec->size()))
ctx->throw_exception("Index out of range", lhs.it_range);
ctx->throw_exception(L("Index out of range"), lhs.it_range);
switch (lhs.opt->type()) {
case coFloats:
check_numeric(rhs);
@@ -1387,11 +1389,11 @@ namespace client
break;
case coBools:
if (rhs.type() != expr::TYPE_BOOL)
ctx->throw_exception("Right side is not a boolean expression", rhs.it_range);
ctx->throw_exception(L("Right side is not a boolean expression"), rhs.it_range);
static_cast<ConfigOptionBools*>(vec)->values[lhs.index] = rhs.b();
break;
default:
ctx->throw_exception("Unsupported output vector variable type", lhs.it_range);
ctx->throw_exception(L("Unsupported output vector variable type"), lhs.it_range);
}
}
@@ -1414,7 +1416,7 @@ namespace client
break;
case coBools:
if (rhs_value.type() != expr::TYPE_BOOL)
rhs_value.throw_exception("Right side is not a boolean expression");
rhs_value.throw_exception(L("Right side is not a boolean expression"));
static_cast<ConfigOptionBools*>(opt)->values.assign(count, rhs_value.b());
break;
default: assert(false);
@@ -1439,10 +1441,10 @@ namespace client
if (ctx->skipping()) {
} else if (opt.opt->is_vector()) {
if (! opt.has_index())
ctx->throw_exception("Referencing a vector variable when scalar is expected", opt.it_range);
ctx->throw_exception(L("Referencing a vector variable when scalar is expected"), opt.it_range);
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt.opt);
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", opt.it_range);
ctx->throw_exception(L("Indexing an empty vector variable"), opt.it_range);
output.set_b(static_cast<const ConfigOptionVectorBase*>(opt.opt)->is_nil(opt.index >= int(vec->size()) ? 0 : size_t(opt.index)));
} else {
assert(opt.opt->is_scalar());
@@ -1466,19 +1468,19 @@ namespace client
if (! ctx->skipping()) {
t_config_option_key key(std::string(it_range.begin(), it_range.end()));
if (const ConfigOption* opt = ctx->resolve_symbol(key); opt)
ctx->throw_exception("Symbol is already defined in read-only system dictionary", it_range);
ctx->throw_exception(L("Symbol is already defined in read-only system dictionary"), it_range);
if (ctx->config_outputs && ctx->config_outputs->optptr(key))
ctx->throw_exception("Symbol is already defined as system output variable", it_range);
ctx->throw_exception(L("Symbol is already defined as system output variable"), it_range);
bool has_global_dictionary = ctx->context_data != nullptr && ctx->context_data->global_config;
if (global_variable) {
if (! has_global_dictionary)
ctx->throw_exception("Global variables are not available in this context", it_range);
ctx->throw_exception(L("Global variables are not available in this context"), it_range);
if (ctx->config_local.optptr(key))
ctx->throw_exception("Variable name already defined in local scope", it_range);
ctx->throw_exception(L("Variable name already defined in local scope"), it_range);
out.opt = ctx->context_data->global_config->optptr(key);
} else {
if (has_global_dictionary && ctx->context_data->global_config->optptr(key))
ctx->throw_exception("Variable name already defined in global scope", it_range);
ctx->throw_exception(L("Variable name already defined in global scope"), it_range);
out.opt = ctx->config_local.optptr(key);
}
out.name = std::move(key);
@@ -1514,7 +1516,7 @@ namespace client
if (ctx->skipping()) {
} else if (lhs.opt) {
if (lhs.opt->is_vector())
rhs.throw_exception("Cannot assign a scalar value to a vector variable.");
rhs.throw_exception(L("Cannot assign a scalar value to a vector variable."));
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
scalar_variable_assign_scalar(ctx, lhs_opt, rhs);
} else {
@@ -1540,7 +1542,7 @@ namespace client
if (ctx->skipping()) {
} else if (lhs.opt) {
if (lhs.opt->is_scalar())
rhs_value.throw_exception("Cannot assign a vector value to a scalar variable.");
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
OptWithPos lhs_opt{ lhs.opt, lhs.it_range, true };
vector_variable_assign_expr_with_count(ctx, lhs_opt, rhs_count, rhs_value);
} else {
@@ -1566,7 +1568,7 @@ namespace client
if (! ctx->skipping()) {
check_writable(ctx, lhs);
if (lhs.opt->is_scalar())
rhs_value.throw_exception("Cannot assign a vector value to a scalar variable.");
rhs_value.throw_exception(L("Cannot assign a vector value to a scalar variable."));
vector_variable_assign_expr_with_count(ctx, lhs, rhs_count, rhs_value);
}
}
@@ -1595,13 +1597,13 @@ namespace client
// scalar_var = ()
// or
// scalar_var = ( scalar, scalar, ... )
ctx->throw_exception("Cannot assign a vector value to a scalar variable.", lhs.it_range);
ctx->throw_exception(L("Cannot assign a vector value to a scalar variable."), lhs.it_range);
}
auto check_numeric_vector = [](const std::vector<expr> &il) {
for (auto &i : il)
if (! i.numeric_type())
i.throw_exception("Right side is not a numeric expression");
i.throw_exception(L("Right side is not a numeric expression"));
};
ConfigOption *opt = const_cast<ConfigOption*>(lhs.opt);
@@ -1620,7 +1622,7 @@ namespace client
case coBools:
for (auto &i : il)
if (i.type() != expr::TYPE_BOOL)
i.throw_exception("Right side is not a boolean expression");
i.throw_exception(L("Right side is not a boolean expression"));
fill_vector_from_initializer_list<ConfigOptionBools>(opt, il, [](auto &v){ return v.b(); });
break;
default: assert(false);
@@ -1642,7 +1644,7 @@ namespace client
vector_variable_assign_initializer_list(ctx, lhs_opt, il);
} else {
if (il.empty())
ctx->throw_exception("Cannot create vector variable from an empty initializer list, because its type cannot be deduced.", lhs.it_range);
ctx->throw_exception(L("Cannot create vector variable from an empty initializer list, because its type cannot be deduced."), lhs.it_range);
// Allocate a new vector variable.
// First guesstimate type of the output vector.
size_t num_bool = 0;
@@ -1663,7 +1665,7 @@ namespace client
opt_new = std::make_unique<ConfigOptionStrings>();
else if (num_bool > 0) {
if (num_double + num_int > 0)
ctx->throw_exception("Right side is not valid: Mixing numeric and boolean types.", IteratorRange{ il.front().it_range.begin(), il.back().it_range.end() });
ctx->throw_exception(L("Right side is not valid: Mixing numeric and boolean types."), IteratorRange{ il.front().it_range.begin(), il.back().it_range.end() });
opt_new = std::make_unique<ConfigOptionBools>();
} else {
// Output is numeric.
@@ -1695,22 +1697,22 @@ namespace client
check_writable(ctx, lhs);
assert(lhs.opt->is_vector());
if (rhs.has_index() || ! rhs.opt->is_vector())
ctx->throw_exception("Cannot assign scalar to a vector", lhs.it_range);
ctx->throw_exception(L("Cannot assign scalar to a vector"), lhs.it_range);
if (rhs.opt->is_nil())
ctx->throw_exception("Some elements of the right hand side vector variable of optional values are undefined (nil)", rhs.it_range);
ctx->throw_exception(L("Some elements of the right hand side vector variable of optional values are undefined (nil)"), rhs.it_range);
if (lhs.opt->type() != rhs.opt->type()) {
// Vector types are not compatible.
switch (lhs.opt->type()) {
case coFloats:
ctx->throw_exception("Left hand side is a float vector, while the right hand side is not.", lhs.it_range);
ctx->throw_exception(L("Left hand side is a float vector, while the right hand side is not."), lhs.it_range);
case coInts:
ctx->throw_exception("Left hand side is an int vector, while the right hand side is not.", lhs.it_range);
ctx->throw_exception(L("Left hand side is an int vector, while the right hand side is not."), lhs.it_range);
case coStrings:
ctx->throw_exception("Left hand side is a string vector, while the right hand side is not.", lhs.it_range);
ctx->throw_exception(L("Left hand side is a string vector, while the right hand side is not."), lhs.it_range);
case coBools:
ctx->throw_exception("Left hand side is a bool vector, while the right hand side is not.", lhs.it_range);
ctx->throw_exception(L("Left hand side is a bool vector, while the right hand side is not."), lhs.it_range);
default:
ctx->throw_exception("Left hand side / right hand side vectors are not compatible.", lhs.it_range);
ctx->throw_exception(L("Left hand side / right hand side vectors are not compatible."), lhs.it_range);
}
}
const_cast<ConfigOption*>(lhs.opt)->set(rhs.opt);
@@ -1735,7 +1737,7 @@ namespace client
// Stop parsing, let the other rules resolve this case.
return false;
if (rhs.opt->is_nil())
ctx->throw_exception("Some elements of the right hand side vector variable of optional values are undefined (nil)", rhs.it_range);
ctx->throw_exception(L("Some elements of the right hand side vector variable of optional values are undefined (nil)"), rhs.it_range);
// Clone the vector variable.
std::unique_ptr<ConfigOption> opt_new;
if (one_of(rhs.opt->type(), { coFloats, coInts, coStrings, coBools }))
@@ -1743,7 +1745,7 @@ namespace client
else if (rhs.opt->type() == coPercents)
opt_new = std::make_unique<ConfigOptionFloats>(static_cast<const ConfigOptionPercents*>(rhs.opt)->values);
else
ctx->throw_exception("Duplicating this type of vector variable is not supported", rhs.it_range);
ctx->throw_exception(L("Duplicating this type of vector variable is not supported"), rhs.it_range);
const_cast<MyContext*>(ctx)->store_new_variable(lhs.name, std::move(opt_new), global_variable);
}
// Continue parsing.
@@ -1761,7 +1763,7 @@ namespace client
{
if (! ctx->skipping()) {
if (opt.has_index() || ! opt.opt->is_vector())
ctx->throw_exception("parameter of empty() is not a vector variable", opt.it_range);
ctx->throw_exception(L("parameter of empty() is not a vector variable"), opt.it_range);
out.set_b(static_cast<const ConfigOptionVectorBase*>(opt.opt)->size() == 0);
}
out.it_range = opt.it_range;
@@ -1771,7 +1773,7 @@ namespace client
{
if (! ctx->skipping()) {
if (opt.has_index() || ! opt.opt->is_vector())
ctx->throw_exception("parameter of size() is not a vector variable", opt.it_range);
ctx->throw_exception(L("parameter of size() is not a vector variable"), opt.it_range);
out.set_i(int(static_cast<const ConfigOptionVectorBase*>(opt.opt)->size()));
}
out.it_range = opt.it_range;
@@ -1783,7 +1785,7 @@ namespace client
{
if (expr_index.type() != expr::TYPE_EMPTY) {
if (expr_index.type() != expr::TYPE_INT)
expr_index.throw_exception("Non-integer index is not allowed to address a vector variable.");
expr_index.throw_exception(L("Non-integer index is not allowed to address a vector variable."));
output = expr_index.i();
}
}
@@ -1794,7 +1796,7 @@ namespace client
return;
if (ctx->context_data == nullptr)
ctx->throw_exception("Random number generator not available in this context.",
ctx->throw_exception(L("Random number generator not available in this context."),
IteratorRange(param1.it_range.begin(), param2.it_range.end()));
expr::random(param1, param2, ctx->context_data->rng);
}
@@ -1832,21 +1834,20 @@ namespace client
auto error_line = std::string(first, first_pos) + std::string(last, 0, last_pos);
// Position of the it_error from the start of its line.
auto error_pos = (it_error - it_begin) - first_pos;
msg += "Parsing error at line " + std::to_string(line_nr);
if (! info.tag.empty() && info.tag.front() == '*') {
// The gat contains an explanatory string.
msg += ": ";
msg += info.tag.substr(1);
// TRN Custom G-code error. %1% is the line number, %2% the reason.
msg += Slic3r::format(_u8L("Parsing error at line %1%: %2%"), line_nr, Slic3r::I18N::translate(info.tag.substr(1)));
} else {
auto it = tag_to_error_message.find(info.tag);
if (it == tag_to_error_message.end()) {
// A generic error report based on the nonterminal or terminal symbol name.
msg += ". Expecting tag ";
msg += info.tag;
// TRN Custom G-code error. %1% is the line number, %2% the name of a grammar rule.
msg += Slic3r::format(_u8L("Parsing error at line %1%. Expecting tag %2%"), line_nr, info.tag);
} else {
// Use the human readable error message.
msg += ". ";
msg += it->second;
// TRN Custom G-code error. %1% is the line number, %2% the reason.
msg += Slic3r::format(_u8L("Parsing error at line %1%. %2%"), line_nr, Slic3r::I18N::translate(it->second));
}
}
msg += '\n';
@@ -1877,15 +1878,15 @@ namespace client
static void init(const expr &x) {
if (x.type() != expr::TYPE_EMPTY) {
if (!x.numeric_type())
x.throw_exception("Interpolation value must be a number.");
x.throw_exception(L("Interpolation value must be a number."));
}
}
static void add_pair(const expr &x, const expr &y, InterpolateTableContext &table) {
if (x.type() != expr::TYPE_EMPTY) {
if (! x.numeric_type())
x.throw_exception("X value of a table point must be a number.");
x.throw_exception(L("X value of a table point must be a number."));
if (! y.numeric_type())
y.throw_exception("Y value of a table point must be a number.");
y.throw_exception(L("Y value of a table point must be a number."));
table.table.push_back({ x.as_d(), x.it_range, y.as_d() });
}
}
@@ -1902,7 +1903,7 @@ namespace client
double x1 = table.table[i].x;
if (x0 > x1)
boost::throw_exception(qi::expectation_failure(
table.table[i - 1].it_range_x.begin(), table.table[i].it_range_x.end(), spirit::info("X coordinates of the table must be increasing")));
table.table[i - 1].it_range_x.begin(), table.table[i].it_range_x.end(), spirit::info(std::string("*") + L("X coordinates of the table must be increasing"))));
if (! evaluated && x >= x0 && x <= x1) {
double y0 = table.table[i - 1].y;
double y1 = table.table[i].y;
@@ -1925,7 +1926,7 @@ namespace client
out.set_d(table.table.back().y);
else
// The value is really outside the table range.
expr_x.throw_exception("Interpolation value is outside the table range");
expr_x.throw_exception(L("Interpolation value is outside the table range"));
}
}
};
@@ -1939,30 +1940,30 @@ namespace client
// Table to translate symbol tag to a human readable error message.
std::map<std::string, std::string> MyContext::tag_to_error_message = {
{ "eoi", "Unknown syntax error" },
{ "start", "Unknown syntax error" },
{ "text", "Invalid text." },
{ "text_block", "Invalid text block." },
{ "macro", "Invalid macro." },
{ "repeat", "Unknown syntax error" },
{ "if_else_output", "Not an {if}{else}{endif} macro." },
{ "switch_output", "Not a {switch} macro." },
{ "legacy_variable_expansion", "Expecting a legacy variable expansion format" },
{ "identifier", "Expecting an identifier." },
{ "conditional_expression", "Expecting a conditional expression." },
{ "logical_or_expression", "Expecting a boolean expression." },
{ "logical_and_expression", "Expecting a boolean expression." },
{ "equality_expression", "Expecting an expression." },
{ "bool_expr_eval", "Expecting a boolean expression."},
{ "relational_expression", "Expecting an expression." },
{ "additive_expression", "Expecting an expression." },
{ "multiplicative_expression", "Expecting an expression." },
{ "unary_expression", "Expecting an expression." },
{ "optional_parameter", "Expecting a closing brace or an optional parameter." },
{ "one_of_list", "Expecting a list of string patterns (simple text or rexep)" },
{ "variable_reference", "Expecting a variable reference."},
{ "variable", "Expecting a variable name."},
{ "regular_expression", "Expecting a regular expression."}
{ "eoi", L("Unknown syntax error") },
{ "start", L("Unknown syntax error") },
{ "text", L("Invalid text.") },
{ "text_block", L("Invalid text block.") },
{ "macro", L("Invalid macro.") },
{ "repeat", L("Unknown syntax error") },
{ "if_else_output", L("Not an {if}{else}{endif} macro.") },
{ "switch_output", L("Not a {switch} macro.") },
{ "legacy_variable_expansion", L("Expecting a legacy variable expansion format") },
{ "identifier", L("Expecting an identifier.") },
{ "conditional_expression", L("Expecting a conditional expression.") },
{ "logical_or_expression", L("Expecting a boolean expression.") },
{ "logical_and_expression", L("Expecting a boolean expression.") },
{ "equality_expression", L("Expecting an expression.") },
{ "bool_expr_eval", L("Expecting a boolean expression.")},
{ "relational_expression", L("Expecting an expression.") },
{ "additive_expression", L("Expecting an expression.") },
{ "multiplicative_expression", L("Expecting an expression.") },
{ "unary_expression", L("Expecting an expression.") },
{ "optional_parameter", L("Expecting a closing brace or an optional parameter.") },
{ "one_of_list", L("Expecting a list of string patterns (simple text or regexp)") },
{ "variable_reference", L("Expecting a variable reference.")},
{ "variable", L("Expecting a variable name.")},
{ "regular_expression", L("Expecting a regular expression.")}
};
// For debugging the boost::spirit parsers. Print out the string enclosed in it_range.
@@ -2031,7 +2032,7 @@ namespace client
first = it;
return true;
err:
MyContext::throw_exception("Invalid utf8 sequence", IteratorRange(first, last));
MyContext::throw_exception(L("Invalid utf8 sequence"), IteratorRange(first, last));
return false;
}
@@ -2057,7 +2058,7 @@ namespace client
return false;
char c = *first;
if (it - first > 1 || c < 0)
MyContext::throw_exception("Non-ASCII7 characters are only allowed inside text blocks and string literals, not inside code blocks.", IteratorRange(first, it));
MyContext::throw_exception(L("Non-ASCII7 characters are only allowed inside text blocks and string literals, not inside code blocks."), IteratorRange(first, it));
if (c == '\r' || c == '\n' || c == '\t' || c == ' ') {
// Skip the whitespaces
++ first;
@@ -2116,7 +2117,7 @@ namespace client
if (*it == '\\') {
if (++ it == end ||
(*it != 'r' && *it != 'n' && *it != '"' && *it != '\\'))
ctx->throw_exception("Invalid escape sequence", {std::prev(it), std::next(it) });
ctx->throw_exception(L("Invalid escape sequence"), {std::prev(it), std::next(it) });
++ len;
++ it;
} else {
+132 -7
View File
@@ -36,6 +36,7 @@
#include "Semver.hpp"
#include "PrintConfig.hpp"
#include "libslic3r_version.h"
#include "I18N.hpp"
#ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN
@@ -81,6 +82,7 @@
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "InstanceLock.hpp"
#include "nlohmann/json.hpp"
#include <sstream>
#include "PlaceholderParser.hpp"
@@ -1820,7 +1822,7 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
const boost::filesystem::path &file, const std::string &canonical_name,
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
const std::string &extruder_id_name, const std::string &extruder_variant_name,
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const
std::set<std::string> *key_set1, std::set<std::string> *key_set2, bool read_only) const
{
UserPresetLoad out;
out.preset = Preset(m_type, canonical_name, false);
@@ -1854,7 +1856,10 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str);
if (!version) return out;
if (!version) {
out.errors.push_back((boost::format("invalid version %1% in config %2%") % version_str % preset.file).str());
return out;
}
preset.version = *version;
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
@@ -1876,6 +1881,12 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
inherit_preset = this->find_preset2(inherits_value);
Preset::normalize_inherits(config, inherit_preset);
}
// Local hot reload only supports FFF; no SLA default printer is installed.
if (read_only && load_origin.kind == PresetOrigin::Kind::LocalBundle && m_type == Preset::TYPE_PRINTER &&
Preset::printer_technology(config) != ptFFF) {
out.errors.push_back((boost::format("local bundle printer %1% is not FFF") % preset.file).str());
return out;
}
const Preset& default_preset = this->default_preset_for(config);
if (inherit_preset) {
preset.config = inherit_preset->config;
@@ -2040,7 +2051,7 @@ void PresetCollection::load_presets(
resolve_then_commit<CNumericLocalesSetter>(files.size(),
[&](size_t i) {
return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
extruder_id_name, extruder_variant_name, key_set1, key_set2);
extruder_id_name, extruder_variant_name, key_set1, key_set2, read_only);
},
[&](size_t i, UserPresetLoad &&loaded) {
// Resolve read the files without the lock, so another instance may have saved
@@ -2060,7 +2071,7 @@ void PresetCollection::load_presets(
if (! (on_disk == loaded.on_disk)) {
CNumericLocalesSetter locales_setter;
loaded = this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
extruder_id_name, extruder_variant_name, key_set1, key_set2);
extruder_id_name, extruder_variant_name, key_set1, key_set2, read_only);
}
}
const bool leave_files = read_only || (! lock_path.empty() && ! instance_lock.locked());
@@ -2757,6 +2768,118 @@ void PresetCollection::update_after_user_presets_loaded()
return;
}
// A user preset file as written: it holds only the keys the preset sets itself, and an inherits that names a
// renamed parent by the old name the preset was made against. Throws if the file can not be read or parsed.
static json read_user_preset_file(const std::string &file)
{
boost::nowide::ifstream in(file);
return json::parse(in);
}
std::vector<LegacyUserFilamentPreset> PresetCollection::legacy_user_filament_presets(const std::map<std::string, std::vector<std::string>> &printers_by_parent)
{
auto has_entry = [&printers_by_parent](const std::string &name) { return printers_by_parent.count(name) > 0; };
std::vector<LegacyUserFilamentPreset> legacy_presets;
// The preset sync thread can add or delete presets meanwhile.
lock();
for (const Preset &preset : m_presets) {
if (!preset.is_user() || preset.inherits().empty())
continue;
const Preset *parent = get_preset_parent(preset);
if (parent == nullptr || !parent->is_system)
continue;
// An empty list already covers every printer.
const std::vector<std::string> &own = preset.config.option<ConfigOptionStrings>("compatible_printers")->values;
if (own.empty())
continue;
// The parent's name in the older release is its current name or one in its renamed_from, so only the
// files of presets whose parent has an entry under one of those are read.
if (!has_entry(parent->name) && std::none_of(parent->renamed_from.begin(), parent->renamed_from.end(), has_entry))
continue;
LegacyUserFilamentPreset legacy{preset.name, parent->name, {}, {}, preset.file, own};
try {
const json j = read_user_preset_file(preset.file);
// A preset whose file sets compatible_printers, even to an empty list, was given its printers on
// purpose and keeps exactly those.
if (j.contains("compatible_printers"))
continue;
legacy.inherits = j.value(BBL_JSON_KEY_INHERITS, std::string());
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": can not read " << preset.file << ": " << err.what();
continue;
}
auto it = printers_by_parent.find(legacy.inherits);
if (it == printers_by_parent.end())
continue;
for (const std::string &printer : it->second)
if (std::find(own.begin(), own.end(), printer) == own.end()) {
legacy.printers.push_back(printer);
legacy.compatible_printers.push_back(printer);
}
if (!legacy.printers.empty())
legacy_presets.push_back(std::move(legacy));
}
unlock();
return legacy_presets;
}
bool PresetCollection::write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy)
{
// Only compatible_printers changes in the file. Saving the whole preset would also rewrite an inherits that
// names a renamed parent by its old name, the only name older releases know it by.
try {
json j = read_user_preset_file(legacy.file);
// Changed by hand or by another instance since the preset was found: a list of its own stands, and
// another parent would need other printers.
if (j.contains("compatible_printers") || j.value(BBL_JSON_KEY_INHERITS, std::string()) != legacy.inherits)
return false;
j["compatible_printers"] = legacy.compatible_printers;
if (const std::error_code ec = write_file_atomically(legacy.file, j.dump(1, '\t') + "\n")) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << legacy.file << ": " << ec.message();
return false;
}
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not migrate " << legacy.file << ": " << err.what();
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": user preset " << legacy.preset << " gets " << legacy.printers.size()
<< " printer(s) its parent listed in an older release";
return true;
}
void PresetCollection::apply_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &written)
{
lock();
for (const LegacyUserFilamentPreset &legacy : written) {
auto it = this->find_preset_internal(legacy.preset);
if (it == m_presets.end() || it->name != legacy.preset)
continue; // deleted by the preset sync thread meanwhile
std::vector<std::string> &saved = it->config.option<ConfigOptionStrings>("compatible_printers", true)->values;
std::vector<std::string> &edited = m_edited_preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values;
// Added to the list the preset holds now rather than replacing it: the cloud sync may have given the
// preset a newer list since it was found, and that list is still to be written out.
std::vector<std::string> printers = saved;
for (const std::string &printer : legacy.printers)
if (std::find(printers.begin(), printers.end(), printer) == printers.end())
printers.push_back(printer);
// A change to the list pending in the settings tab stays pending. Without one, the edited copy and the
// snapshot unsaved changes are measured against take the written list, so nothing reads as modified.
if (m_idx_selected == static_cast<size_t>(it - m_presets.begin()) && edited == saved) {
edited = printers;
if (m_saved_preset.name == it->name)
m_saved_preset.config.option<ConfigOptionStrings>("compatible_printers", true)->values = printers;
}
saved = printers;
// Push the change to the cloud copy, as an edit in the settings tab would. A sync already pending for
// the preset (a download waiting to be saved, a deletion) keeps its state.
if (!it->setting_id.empty() && it->sync_info.empty()) {
it->sync_info = "update";
it->save_info();
}
}
unlock();
}
//BBS: validate_preset
bool PresetCollection::validate_preset(const std::string &preset_name, std::string &inherit_name)
{
@@ -3021,7 +3144,9 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
}
// Insert a new profile.
//BBS: add project embedded preset logic
bool from_project = boost::algorithm::iends_with(name, ".3mf");
// A G-code's settings are shown like a project's, so a preset made from them lives with the project
// rather than in the user's library.
bool from_project = boost::algorithm::iends_with(name, ".3mf") || is_gcode_file(name);
if (m_type == Preset::TYPE_PRINT)
cfg.option<ConfigOptionString >("print_settings_id", true)->value = new_name;
else if (m_type == Preset::TYPE_FILAMENT)
@@ -4640,10 +4765,10 @@ void PhysicalPrinterCollection::load_printers(
printer.loaded = true;
}
catch (const std::ifstream::failure& err) {
throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + printer.file + "\n\tReason: " + err.what());
throw Slic3r::RuntimeError((boost::format(_u8L("The selected preset cannot be loaded: %1%\n\tReason: %2%")) % printer.file % err.what()).str());
}
catch (const std::runtime_error& err) {
throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + printer.file + "\n\tReason: " + err.what());
throw Slic3r::RuntimeError((boost::format(_u8L("Failed loading the preset file: %1%\n\tReason: %2%")) % printer.file % err.what()).str());
}
printers_loaded.emplace_back(printer);
}
+28 -1
View File
@@ -551,6 +551,20 @@ struct PresetConfigSubstitutions {
// PrusaSlicer and reading the user Print / Filament / Printer profiles.
using PresetsConfigSubstitutions = std::vector<PresetConfigSubstitutions>;
// A user filament preset made against an older release that no longer shows on printers its system parent
// listed then, and those printers. Migrating it writes them into the preset's own compatible_printers.
struct LegacyUserFilamentPreset {
std::string preset;
std::string parent;
// The parent's name as the preset file states it: the name the older release knew it by.
std::string inherits;
// The printers the preset lost: what the prompt lists.
std::vector<std::string> printers;
std::string file;
// The list the file gets: the parent's current printers plus the lost ones.
std::vector<std::string> compatible_printers;
};
// Collections of presets of the same type (one of the Print, Filament or Printer type).
class PresetCollection
{
@@ -601,6 +615,18 @@ public:
void save_user_presets(const std::string& dir_path, const std::string& type, std::map<std::string, std::string>& need_to_delete_list);
bool load_user_preset(std::string name, std::map<std::string, std::string> preset_values, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, const PresetOrigin &load_origin = PresetOrigin(PresetOrigin::Kind::User));
void update_after_user_presets_loaded();
// User presets store only their diff, so a user preset inherits its system parent's compatible_printers
// and disappears from a printer once a profile update narrows that list. Lists the user presets whose
// files do not set compatible_printers, and the printers their parent covered in an older release but
// no longer lists. printers_by_parent is keyed by the parent's name in that release, which is the name
// the preset file's inherits still holds.
std::vector<LegacyUserFilamentPreset> legacy_user_filament_presets(const std::map<std::string, std::vector<std::string>> &printers_by_parent);
// Writes legacy.compatible_printers into the preset's file and keeps every other key. The caller holds the
// user presets lock. False if the file has gained a compatible_printers key or another parent since the
// preset was found, or could not be written.
static bool write_legacy_user_filament_preset(const LegacyUserFilamentPreset &legacy);
// Gives the loaded presets the lists their files were just given, and marks cloud copies for upload.
void apply_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &written);
//BBS: get user presets
int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &result_presets);
void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id);
@@ -965,7 +991,8 @@ private:
UserPresetLoad resolve_user_preset(const boost::filesystem::path &file, const std::string &canonical_name,
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
const std::string &extruder_id_name, const std::string &extruder_variant_name,
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const;
std::set<std::string> *key_set1, std::set<std::string> *key_set2,
bool read_only) const;
// Install one resolved preset. The collection, its alias maps, the error count
// and the preset files on disk are touched here and only here.
+566 -70
View File
@@ -1,7 +1,9 @@
#include <array>
#include <atomic>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/directory.hpp>
#include <boost/filesystem/file_status.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/optional/optional.hpp>
#include <boost/algorithm/string/split.hpp>
@@ -38,6 +40,8 @@
#include "ParallelResolve.hpp"
#include "Preset.hpp"
#include "PresetCacheFormat.hpp"
#include "Time.hpp"
#include "Zipper.hpp"
#include "PrintConfig.hpp"
#include "PublishSettings.hpp"
#include "FilamentMixer.hpp"
@@ -53,8 +57,10 @@
#include <algorithm>
#include <cstdlib>
#include <mutex>
#include <numeric>
#include <set>
#include <shared_mutex>
#include <fstream>
#include <string>
#include <tuple>
@@ -96,6 +102,21 @@ using json = nlohmann::json;
namespace Slic3r {
// Per system filament preset of an older release, by its name then, the printers it listed that the preset it
// now loads as does not (resources/info/legacy_compatible_printers.json, written by orca_profile_tool.py
// legacy-compat). Empty if the file is missing or malformed.
static std::map<std::string, std::vector<std::string>> legacy_compatible_printers()
{
const fs::path path = fs::path(resources_dir()) / "info" / "legacy_compatible_printers.json";
try {
boost::nowide::ifstream in(path.string());
return json::parse(in).at("presets").get<std::map<std::string, std::vector<std::string>>>();
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": can not read " << path.string() << ": " << err.what();
return {};
}
}
// Project-level options imported from a loaded 3MF into project_config. s_project_options_published
// below is the reduced subset that crosses over in "published" 3MF mode; keep both in sync.
// s_project_options_published additionally carries wipe_tower_rotation_angle, which normal
@@ -809,7 +830,7 @@ void PresetBundle::setup_directories()
BOOST_LOG_TRIVIAL(warning) << boost::format("creating directory %1% failed, maybe created by other instance, go on!")%subdir.string();
}
else
throw Slic3r::RuntimeError(std::string("Unable to create directory ") + subdir.string());
throw Slic3r::RuntimeError((boost::format(L("Unable to create directory %1%")) % subdir.string()).str());
}
}
}
@@ -1045,6 +1066,73 @@ bool PresetBundle::backup_user_folder() const
}
}
bool PresetBundle::backup_user_folder_to_zip(const std::string &zip_path) const
{
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR;
BOOST_LOG_TRIVIAL(info) << "Backing up user folder to: " << zip_path;
try {
Zipper zipper(zip_path);
// Directories get their own entries, which extract_archive_confined() needs to recreate them.
zipper.add_entry(std::string(PRESET_USER_DIR) + "/", nullptr, 0);
// The loader follows symlinked folders, and migration writes the presets in them.
for (const fs::directory_entry &entry : fs::recursive_directory_iterator(user_dir, fs::directory_options::follow_directory_symlink)) {
const std::string name = (fs::path(PRESET_USER_DIR) / entry.path().lexically_relative(user_dir)).generic_string();
if (fs::is_directory(entry.status())) {
zipper.add_entry(name + "/", nullptr, 0);
continue;
}
if (!fs::is_regular_file(entry.status()))
continue;
boost::nowide::ifstream in(entry.path().string(), std::ios::binary);
const std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!in.good() && !in.eof())
throw Slic3r::RuntimeError("can not read " + entry.path().string());
zipper.add_entry(name, data.data(), data.size());
}
zipper.finalize();
return true;
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "Exception during user folder backup: " << ex.what();
boost::system::error_code ec;
fs::remove(zip_path, ec);
return false;
}
}
std::vector<LegacyUserFilamentPreset> PresetBundle::legacy_user_filament_presets()
{
return filaments.legacy_user_filament_presets(legacy_compatible_printers());
}
PresetBundle::LegacyUserFilamentPresetMigration PresetBundle::migrate_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &legacy_presets)
{
LegacyUserFilamentPresetMigration result;
std::vector<LegacyUserFilamentPreset> written;
const std::string zip_path = data_dir() + "/filament_preset_migration_" +
Utils::time2str(Utils::TimeZone::utc, Utils::TimeFormat::iso8601Z) + "_backup.zip";
// Before the lock: zipping a large folder takes longer than a guard may be held.
if (!backup_user_folder_to_zip(zip_path))
return result;
result.backup = zip_path;
{
// One hold for every write, so a lock another instance keeps is one outcome rather than a failure per
// preset (the lock backs off for a while after a miss). Held without the collection mutex, the order
// InstanceLock requires; each write re-reads its file, so a save that slipped in before is respected.
const std::string lock_path = user_presets_lock_path();
InstanceLock instance_lock(lock_path);
if (!lock_path.empty() && !instance_lock.locked())
return result;
result.locked = true;
for (const LegacyUserFilamentPreset &legacy : legacy_presets)
if (PresetCollection::write_legacy_user_filament_preset(legacy))
written.push_back(legacy);
else
result.failed.push_back(legacy.preset);
}
filaments.apply_legacy_user_filament_presets(written);
return result;
}
std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const std::string& filament_id, const std::string& printer_name) const
{
if (filament_id.empty())
@@ -1298,6 +1386,27 @@ std::string PresetBundle::get_hotend_model_for_printer_model(std::string model_n
return out;
}
static void load_bundle_presets(PresetBundle& bundle, const std::string& bundle_dir, BundleMetadata& metadata,
const PresetOrigin& origin, PresetsConfigSubstitutions& substitutions,
ForwardCompatibilitySubstitutionRule substitution_rule, bool read_only = false)
{
struct Collection {
PresetCollection& presets;
const char* directory;
std::vector<std::string>& names;
};
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
for (Collection collection : {
Collection{ bundle.prints, PRESET_PRINT_NAME, metadata.print_presets },
Collection{ bundle.filaments, PRESET_FILAMENT_NAME, metadata.filament_presets },
Collection{ bundle.printers, PRESET_PRINTER_NAME, metadata.printer_presets } })
collection.presets.load_presets(bundle_dir, collection.directory, substitutions, substitution_rule,
[&](Preset& preset) { collection.names.push_back(preset.name); }, origin, read_only);
}
PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule substitution_rule, bool read_only)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry and user is: " << user;
@@ -1346,19 +1455,8 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
BundleMetadata metadata;
if (!load_bundle_metadata(metadata_file, metadata)) continue;
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.filament_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.printer_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
load_bundle_presets(*this, bundle_dir, metadata, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id),
substitutions, substitution_rule, read_only);
metadata.bundle_type = BundleType::Local;
metadata.path = metadata_file.string();
@@ -1381,20 +1479,10 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
BundleMetadata metadata;
if (!load_bundle_metadata(metadata_file, metadata)) continue;
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
metadata.is_subscribed = true;
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.filament_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.printer_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
load_bundle_presets(*this, bundle_dir, metadata, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id),
substitutions, substitution_rule, read_only);
metadata.bundle_type = BundleType::Subscribed;
metadata.path = metadata_file.string();
@@ -1449,6 +1537,335 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
return PresetsConfigSubstitutions();
}
static bool is_single_path_component(const std::string& value)
{
const fs::path path(value);
return !value.empty() && value != "." && value != ".." && value.find_first_of("/\\") == std::string::npos &&
!path.has_root_name() && !path.has_root_directory();
}
static bool check_local_bundle(PresetBundleMetadata& bundles, const fs::path& user_dir, const std::string& bundle_id,
BundleMetadata& metadata, std::string& error)
{
if (!is_single_path_component(bundle_id)) {
error = "Local bundle id must be a single path component";
return false;
}
const fs::path bundle_dir = user_dir / PRESET_LOCAL_DIR / bundle_id;
if (!fs::is_directory(bundle_dir)) {
error = "Local bundle directory does not exist";
return false;
}
if (fs::exists(user_dir / PRESET_SUBSCRIBED_DIR / bundle_id)) {
error = "A subscribed bundle already uses this id";
return false;
}
{
std::shared_lock<std::shared_mutex> lock(bundles.RWMtx);
const auto it = bundles.m_bundles.find(bundle_id);
if (it != bundles.m_bundles.end() && (it->second.bundle_type == BundleType::Subscribed || it->second.is_subscribed)) {
error = "A subscribed bundle already uses this id";
return false;
}
}
if (!metadata.load_from_json((bundle_dir / PRESET_BUNDLE_METADATA).string()) || metadata.id != bundle_id) {
error = "Local bundle metadata is missing, malformed, or has a different id";
return false;
}
return true;
}
std::vector<std::string> PresetBundle::list_local_bundle_ids(const std::string& preset_folder)
{
std::vector<std::string> ids;
const std::string user = preset_folder.empty() ? DEFAULT_USER_FOLDER_NAME : preset_folder;
if (!is_single_path_component(user))
return ids;
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR / user;
const fs::path local_dir = user_dir / PRESET_LOCAL_DIR;
try {
if (!fs::is_directory(local_dir))
return ids;
for (const auto& entry : fs::directory_iterator(local_dir)) {
const std::string bundle_id = entry.path().filename().string();
try {
BundleMetadata metadata;
std::string error;
if (check_local_bundle(bundles, user_dir, bundle_id, metadata, error))
ids.push_back(bundle_id);
} catch (const std::exception& exception) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": skipping " << bundle_id << ": " << exception.what();
}
}
} catch (const std::exception& exception) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << exception.what();
}
std::sort(ids.begin(), ids.end());
return ids;
}
bool PresetBundle::reload_local_bundle(const std::string& preset_folder, const std::string& bundle_id, std::string* error)
{
auto fail = [error](const std::string& message) {
if (error != nullptr)
*error = message;
return false;
};
if (!is_single_path_component(bundle_id))
return fail("Local bundle id must be a single path component");
const std::string user = preset_folder.empty() ? DEFAULT_USER_FOLDER_NAME : preset_folder;
if (!is_single_path_component(user))
return fail("Preset folder must be a single path component");
try {
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR / user;
const fs::path bundle_dir = user_dir / PRESET_LOCAL_DIR / bundle_id;
const fs::path metadata_file = bundle_dir / PRESET_BUNDLE_METADATA;
BundleMetadata metadata;
std::string check_error;
if (!check_local_bundle(bundles, user_dir, bundle_id, metadata, check_error))
return fail(check_error);
// Keep background preset sync from changing the collections between copying and committing them.
std::scoped_lock collection_lock(prints.m_mutex, filaments.m_mutex, printers.m_mutex);
const std::string target_prefix = std::string(PRESET_LOCAL_DIR) + "/" + bundle_id + "/";
const auto is_target = [&](const Preset& preset) {
return preset.bundle_id == bundle_id && boost::starts_with(preset.name, target_prefix);
};
struct Selection {
std::string name;
Preset base{Preset::TYPE_INVALID, std::string()};
Preset edited{Preset::TYPE_INVALID, std::string()};
std::vector<std::string> dirty_options;
};
auto snapshot = [&](const PresetCollection& collection, Selection& selection) {
selection.name = collection.get_selected_preset_name();
if (selection.name.empty())
return false;
for (const Preset& preset : collection.get_presets()) {
if (preset.name == selection.name) {
selection.base = preset;
selection.edited = collection.get_edited_preset();
selection.dirty_options = PresetCollection::dirty_options(&selection.edited, &selection.base);
return true;
}
}
return false;
};
Selection print_selection, filament_selection, printer_selection;
if (!snapshot(prints, print_selection) || !snapshot(filaments, filament_selection) || !snapshot(printers, printer_selection))
return fail("Preset selections are not initialized");
PresetBundle staged;
// Collection copies keep vendor pointers into the live vendor map, which is not replaced by this reload.
staged.prints = prints;
staged.filaments = filaments;
staged.printers = printers;
staged.filament_presets = filament_presets;
staged.project_config.apply_only(project_config, {"flush_volumes_matrix", "flush_volumes_vector", "flush_multiplier"});
auto remove_target = [&](PresetCollection& collection) {
auto begin = collection.m_presets.begin() + collection.m_num_default_presets;
collection.m_presets.erase(std::remove_if(begin, collection.m_presets.end(), is_target), collection.m_presets.end());
collection.sort_presets();
};
remove_target(staged.prints);
remove_target(staged.filaments);
remove_target(staged.printers);
const std::array<int, 3> errors_before = { staged.prints.error_count(), staged.filaments.error_count(), staged.printers.error_count() };
PresetsConfigSubstitutions substitutions;
load_bundle_presets(staged, bundle_dir.string(), metadata, PresetOrigin(PresetOrigin::Kind::LocalBundle, bundle_id),
substitutions, ForwardCompatibilitySubstitutionRule::Enable, true);
if (staged.prints.error_count() != errors_before[0] || staged.filaments.error_count() != errors_before[1] || staged.printers.error_count() != errors_before[2])
return fail("Local bundle contains an invalid preset");
auto dirty_target_was_removed = [&](const Selection& selection, const PresetCollection& collection) {
if (selection.dirty_options.empty() || !is_target(selection.base))
return false;
for (const Preset& preset : collection.get_presets())
if (preset.name == selection.name && is_target(preset))
return false;
return true;
};
if (dirty_target_was_removed(print_selection, staged.prints) ||
dirty_target_was_removed(filament_selection, staged.filaments) ||
dirty_target_was_removed(printer_selection, staged.printers))
return fail("A modified selected preset was removed or renamed");
auto external_child_name = [&](const PresetCollection& collection) {
auto depends_on_target = [&](const Preset& preset) {
if (is_target(preset))
return false;
const Preset* parent = &preset;
std::unordered_set<const Preset*> visited;
while ((parent = collection.get_preset_parent(*parent)) != nullptr && visited.insert(parent).second)
if (is_target(*parent))
return true;
for (const char* key : {"compatible_prints", "compatible_printers"}) {
const auto* names = preset.config.option<ConfigOptionStrings>(key);
if (names == nullptr)
continue;
const PresetCollection& referenced = std::string(key) == "compatible_printers" ? printers : prints;
for (const std::string& name : names->values) {
const Preset* dependency = referenced.find_preset(name, false);
if (dependency != nullptr && is_target(*dependency))
return true;
}
}
return false;
};
for (const Preset& preset : collection.get_presets())
if (depends_on_target(preset))
return preset.name;
if (depends_on_target(collection.get_edited_preset()))
return collection.get_edited_preset().name;
return std::string();
};
for (const PresetCollection* collection : std::array<const PresetCollection*, 3>{&prints, &filaments, &printers}) {
const std::string child_name = external_child_name(*collection);
if (!child_name.empty())
return fail("Preset outside this bundle depends on it: " + child_name);
}
auto restore_selection = [](PresetCollection& collection, const Selection& selection) {
if (collection.find_preset(selection.name, false, true) == nullptr) {
collection.select_preset(collection.first_visible_idx());
return false;
}
collection.select_preset_by_name(selection.name, true);
if (collection.get_selected_preset_name() != selection.name)
return false;
Preset& edited = collection.get_edited_preset();
for (const std::string& key : selection.dirty_options) {
if (const ConfigOption* option = selection.edited.config.option(key))
edited.config.set_key_value(key, option->clone());
else
edited.config.erase(key);
}
collection.update_dirty();
return true;
};
staged.prints.update_map_alias_to_profile_name();
staged.filaments.update_map_alias_to_profile_name();
staged.printers.update_map_alias_to_profile_name();
const bool printer_preserved = restore_selection(staged.printers, printer_selection);
const bool print_preserved = restore_selection(staged.prints, print_selection);
const bool filament_preserved = restore_selection(staged.filaments, filament_selection);
const auto selection_mode = [printer_preserved](bool selection_preserved) {
if (!selection_preserved)
return PresetSelectCompatibleType::Always;
return printer_preserved ? PresetSelectCompatibleType::Never : PresetSelectCompatibleType::OnlyIfWasCompatible;
};
if (staged.printers.get_edited_preset().printer_technology() != ptFFF)
return fail("Local bundle reload requires an FFF printer selection");
staged.update_compatible(selection_mode(print_preserved), selection_mode(filament_preserved));
for (std::string& name : staged.filament_presets)
if (boost::starts_with(name, target_prefix) && staged.filaments.find_preset(name, false) == nullptr)
name = staged.filaments.first_compatible().name;
staged.update_multi_material_filament_presets();
struct Collection
{
PresetCollection& live;
PresetCollection& candidate;
size_t original_size;
};
std::array<Collection, 3> collections{{{prints, staged.prints, prints.size()},
{filaments, staged.filaments, filaments.size()},
{printers, staged.printers, printers.size()}}};
for (size_t i = 0; i < collections.size(); ++i) {
const auto& collection = collections[i];
const auto dirty_options = collection.live.current_dirty_options();
if (dirty_options.empty())
continue;
if (collection.live.get_selected_preset_name() != collection.candidate.get_selected_preset_name())
return fail("Reload would discard a modified selected preset");
for (const std::string& key : dirty_options) {
const ConfigOption* before = collection.live.get_edited_preset().config.option(key);
const ConfigOption* after = collection.candidate.get_edited_preset().config.option(key);
if ((before == nullptr) != (after == nullptr) || (before != nullptr && *before != *after))
return fail("Reload would discard a modified preset option: " + key);
}
}
if (calibrate_printer != nullptr && staged.printers.find_preset(calibrate_printer->name, false, true) != nullptr)
staged.set_calibrate_printer(calibrate_printer->name);
std::array<std::pair<ConfigOptionFloats*, ConfigOptionFloats*>, 3> flush_options;
size_t i = 0;
for (const char* key : {"flush_volumes_matrix", "flush_volumes_vector", "flush_multiplier"})
flush_options[i++] = {project_config.option<ConfigOptionFloats>(key), staged.project_config.option<ConfigOptionFloats>(key)};
metadata.bundle_type = BundleType::Local;
metadata.is_subscribed = false;
metadata.update_available = false;
metadata.path = metadata_file.string();
fs::path local_dir = user_dir / PRESET_LOCAL_DIR;
std::unique_lock<std::shared_mutex> lock(bundles.RWMtx);
const auto existing = bundles.m_bundles.find(bundle_id);
if (existing != bundles.m_bundles.end() &&
(existing->second.bundle_type == BundleType::Subscribed || existing->second.is_subscribed))
return fail("A subscribed bundle already uses this id");
std::set<const Preset*> calibration_filaments;
const Preset* calibration_printer = nullptr;
try {
// Growing a deque preserves existing objects. Roll back appended copies if preparation fails.
for (size_t i = 0; i < collections.size(); ++i) {
auto& collection = collections[i];
while (collection.live.size() < collection.candidate.size())
collection.live.m_presets.emplace_back(collection.candidate.m_presets[collection.live.size()]);
}
auto calibration_preset = [](const Preset* preset, PresetCollection& live, PresetCollection& candidate) -> const Preset* {
if (preset == nullptr)
return nullptr;
if (preset == &candidate.get_edited_preset())
return &live.get_edited_preset();
const auto it = candidate.find_preset_internal(preset->name);
return &live.m_presets[it - candidate.m_presets.begin()];
};
calibration_printer = calibration_preset(staged.calibrate_printer, printers, staged.printers);
for (const Preset* preset : staged.calibrate_filaments)
calibration_filaments.insert(calibration_preset(preset, filaments, staged.filaments));
// Insertion is the last allocating operation. Replacing metadata only moves its members.
bundles.m_bundles.insert_or_assign(bundle_id, std::move(metadata));
} catch (...) {
for (size_t i = 0; i < collections.size(); ++i)
while (collections[i].live.size() > collections[i].original_size)
collections[i].live.m_presets.pop_back();
throw;
}
auto install_target = [](PresetCollection& live, PresetCollection& candidate) noexcept {
// Keep stored Preset references alive when the collection's layout is unchanged.
// Adding/removing presets retains the collection's usual sorted-position semantics.
for (size_t i = 0; i < candidate.size(); ++i)
live.m_presets[i] = std::move(candidate.m_presets[i]);
while (live.size() > candidate.size())
live.m_presets.pop_back();
live.m_map_alias_to_profile_name.swap(candidate.m_map_alias_to_profile_name);
// Move assignment does not allocate (including Semver); swapping Presets would construct temporary containers.
live.m_edited_preset = std::move(candidate.m_edited_preset);
live.m_saved_preset = std::move(candidate.m_saved_preset);
live.m_idx_selected = candidate.m_idx_selected;
};
for (size_t i = 0; i < collections.size(); ++i)
install_target(collections[i].live, collections[i].candidate);
filament_presets.swap(staged.filament_presets);
for (const auto& options : flush_options)
options.first->values.swap(options.second->values);
calibrate_printer = calibration_printer;
calibrate_filaments.swap(calibration_filaments);
dir_user_presets_local.swap(local_dir);
return true;
} catch (const std::exception& exception) {
return fail(std::string("Failed to reload local bundle: ") + exception.what());
}
}
PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig & config,
std::map<std::string, std::map<std::string, std::string>> &my_presets,
ForwardCompatibilitySubstitutionRule substitution_rule)
@@ -3813,6 +4230,8 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
// BBS: change directories by design
void PresetBundle::export_selections(AppConfig &config)
{
if (m_selection_export_blockers > 0)
return;
assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1);
//assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front());
config.clear_section("presets");
@@ -5439,23 +5858,94 @@ DynamicPrintConfig PresetBundle::full_sla_config() const
ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule)
{
if (is_gcode_file(path)) {
DynamicPrintConfig config;
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, gcodefile %1%, compatibility_rule %2%")%path %compatibility_rule;
config.apply(FullPrintConfig::defaults());
ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, compatibility_rule);
Preset::normalize(config);
ConfigSubstitutions config_substitutions;
DynamicPrintConfig config = config_from_gcode(path, compatibility_rule, config_substitutions);
load_config_file_config(path, true, std::move(config));
return config_substitutions;
}
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode")%path ;
throw Slic3r::RuntimeError(std::string("Unknown configuration file: ") + path);
throw Slic3r::RuntimeError((boost::format(L("Unknown configuration file: %1%")) % path).str());
return ConfigSubstitutions{};
}
DynamicPrintConfig PresetBundle::config_from_gcode(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule,
ConfigSubstitutions &substitutions) const
{
DynamicPrintConfig config;
config.apply(FullPrintConfig::defaults());
// The config block leaves a few project settings out (filament_colour_type, prime_volume_mode,
// flush_multiplier_fast); those keep this bundle's values rather than falling back to the defaults.
config.apply(this->project_config);
const DynamicPrintConfig base = config;
substitutions = config.load_from_gcode_file(path, compatibility_rule);
// An empty list of choices sets nothing: G-code written by a tool rather than from presets leaves per-extruder
// lists such as nozzle_volume_type empty, and the settings tabs expect a choice per extruder. An empty text
// list, such as filament_multi_colour, is a value of its own and stays.
for (const std::string &key : config.keys())
if (const ConfigOption *opt = config.option(key); opt->type() == coEnums && static_cast<const ConfigOptionVectorBase *>(opt)->size() == 0)
if (const ConfigOption *below = base.option(key); below != nullptr && static_cast<const ConfigOptionVectorBase *>(below)->size() > 0)
config.set_key_value(key, below->clone());
Preset::normalize(config);
// The block holds flush_volumes_matrix already multiplied by the multiplier the slice used, so a
// multiplier of 1 keeps the purge volumes the file was sliced with. G-code from before 2.3.2 held the
// plain matrix and loses a multiplier other than 1.
for (const char *key : { "flush_multiplier", "flush_multiplier_fast" })
if (auto *multiplier = config.option<ConfigOptionFloats>(key))
std::fill(multiplier->values.begin(), multiplier->values.end(), 1.);
return config;
}
std::optional<PresetBundle::Selection> PresetBundle::selection() const
{
auto capture = [](const PresetCollection &presets, Selection::Collection &out) {
if (std::any_of(presets.m_presets.begin(), presets.m_presets.end(), [](const Preset &preset) { return preset.is_project_embedded; }))
return false;
out.selected = presets.get_selected_preset_name();
out.edited = presets.get_edited_preset().config;
out.saved = presets.m_saved_preset.config;
out.visible.reserve(presets.m_presets.size());
for (const Preset &preset : presets.m_presets)
out.visible.push_back(preset.is_visible);
return true;
};
Selection selection;
if (!capture(this->prints, selection.prints) || !capture(this->filaments, selection.filaments) || !capture(this->printers, selection.printers))
return std::nullopt;
selection.filament_presets = this->filament_presets;
selection.project_config = this->project_config;
selection.extruder_ams_counts = this->extruder_ams_counts;
return selection;
}
void PresetBundle::restore_selection(const Selection &selection)
{
auto restore = [](PresetCollection &presets, const Selection::Collection &saved) {
presets.reset_project_embedded_presets();
// Before selecting: select_preset_by_name() passes over a hidden preset.
assert(presets.m_presets.size() == saved.visible.size());
for (size_t i = 0; i < std::min(presets.m_presets.size(), saved.visible.size()); ++i)
presets.m_presets[i].is_visible = saved.visible[i];
presets.select_preset_by_name(saved.selected, true);
presets.m_saved_preset.config = saved.saved;
presets.get_edited_preset().config = saved.edited;
presets.update_dirty();
};
restore(this->printers, selection.printers);
restore(this->prints, selection.prints);
restore(this->filaments, selection.filaments);
this->update_compatible(PresetSelectCompatibleType::Never);
this->update_multi_material_filament_presets();
// Last, as update_multi_material_filament_presets() resizes filament_presets and the flush data in project_config.
this->filament_presets = selection.filament_presets;
this->project_config = selection.project_config;
this->extruder_ams_counts = selection.extruder_ams_counts;
}
//some filament presets split from one to sperate ones
//following map recording these filament presets
@@ -5653,7 +6143,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
ConfigOptionStrings* filament_colour_option = config.option<ConfigOptionStrings>("filament_colour");
size_t num_filaments = filament_colour_option?filament_colour_option->size():0;
if (num_filaments == 0)
throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path);
throw Slic3r::RuntimeError((boost::format(L("Invalid configuration file: %1%")) % name_or_path).str());
#endif
//BBS: add config related logs
@@ -5717,7 +6207,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
|| (extruder_variant_count < num_filaments)) {
assert(false);
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid config file %1%, can not find suitable filament_extruder_variant or filament_self_index") % name_or_path;
throw Slic3r::RuntimeError(std::string("Invalid configuration file: ") + name_or_path);
throw Slic3r::RuntimeError((boost::format(L("Invalid configuration file: %1%")) % name_or_path).str());
}
if (num_filaments != extruder_variant_count) {
process_multi_extruder = true;
@@ -6151,8 +6641,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// A payload mix authored at the same slot (a mix inside the physical region) is
// processed after this sweep and overrides its own destination below.
published_config->mixed_slot_relocations.insert_or_assign(int(slot), int(dest));
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + " -> slot " +
std::to_string(dest) + ": mixed filament");
// TRN Filament slot change after loading a published project. %1% and %2% are slot numbers.
published_config->material_replacements.emplace_back(
(boost::format(L("slot %1% -> slot %2%: mixed filament")) % slot % dest).str());
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated receiver mixed filament slot " << slot
<< " -> " << dest << " (physical-first rebalance)";
}
@@ -6186,9 +6677,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// No free virtual slot left: report instead of destroying a real filament.
// The local skipped_keys is published wholesale at the end of the pass;
// writing published_config->skipped_keys here would be clobbered by it.
skipped_keys.emplace_back("material:" + material_label + (is_payload_mix ?
" (mixed filament definition: filament slot limit reached)" :
" (filament slot limit reached)"));
skipped_keys.emplace_back("material:" + material_label + " (" +
(is_payload_mix ? L("mixed filament definition: filament slot limit reached") :
L("filament slot limit reached")) +
")");
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF " << (is_payload_mix ? "mixed filament definition" : "material")
<< " from slot " << entry.slot << " could not be placed: all " << next_free_slot
<< " slots exhausted";
@@ -6200,8 +6692,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// sidebar's mixed section needs two physical filaments to mix), so a
// placeholder would be invisible and unfixable: drop the entry and
// report it like any other unappliable input.
skipped_keys.emplace_back("material:" + material_label + " (printer supports only " +
std::to_string(physical_capacity) + " filament)");
skipped_keys.emplace_back("material:" + material_label + " (" +
(boost::format(L("the printer's filament limit is %1%")) % physical_capacity).str() + ")");
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF material from slot " << entry.slot
<< " dropped: printer supports only " << physical_capacity << " filament";
entry_it = published_config->material_keys.erase(entry_it);
@@ -6219,8 +6711,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
any_mixed_relocated = true;
mixed_moves.emplace_back(size_t(authored_slot), size_t(entry.slot));
published_config->mixed_slot_relocations.insert_or_assign(authored_slot, entry.slot);
published_config->material_replacements.emplace_back("slot " + std::to_string(authored_slot) + " -> slot " +
std::to_string(entry.slot) + ": mixed filament");
published_config->material_replacements.emplace_back(
(boost::format(L("slot %1% -> slot %2%: mixed filament")) % authored_slot % entry.slot).str());
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF relocated mixed filament slot " << authored_slot
<< " -> " << entry.slot;
++entry_it;
@@ -6238,10 +6730,11 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
published_config->mixed_slot_relocations.insert_or_assign(authored_slot, dest_slot);
}
published_config->material_replacements.emplace_back(
(dest_slot != authored_slot ?
"slot " + std::to_string(authored_slot) + " -> slot " + std::to_string(dest_slot) :
"slot " + std::to_string(dest_slot)) +
": unassigned mixed filament (printer supports only " + std::to_string(physical_capacity) + " filaments)");
dest_slot != authored_slot ?
(boost::format(L("slot %1% -> slot %2%: unassigned mixed filament (the printer's filament limit is %3%)")) %
authored_slot % dest_slot % physical_capacity).str() :
(boost::format(L("slot %1%: unassigned mixed filament (the printer's filament limit is %2%)")) % dest_slot %
physical_capacity).str());
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": published 3MF material from slot " << authored_slot
<< " placed as an unassigned mixed filament at slot " << dest_slot
<< " (printer supports only " << physical_capacity << " filaments)";
@@ -6463,8 +6956,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
this->filament_presets[slot] = replacement;
material_applied = true;
// The re-point used to be silent; surface it like the other slot changes.
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + ": " + aliased_name + " -> " +
replacement);
// TRN Filament slot change after loading a published project. %1% is the slot number, %2% the old preset, %3% the new one.
published_config->material_replacements.emplace_back(
(boost::format(L("slot %1%: %2% -> %3%")) % slot % aliased_name % replacement).str());
}
// Grow the per-slot colour/type/map project vectors to the new slot count and
// seed the new entries so the slots render with colours instead of blank chips
@@ -6659,8 +7153,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// definition below.
if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) {
finalize_mixed_placeholder(slot);
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
": mixed filament definition could not be imported");
published_config->material_replacements.emplace_back(
(boost::format(L("slot %1%: mixed filament definition could not be imported")) % slot).str());
}
continue;
}
@@ -6687,17 +7181,18 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
if (comps.size() < 2)
// An empty definition cell counts as broken too: applying it
// would ship a mix the sidebar would only flag later.
mix_error = "needs at least two components";
mix_error = L("needs at least two components");
else
for (unsigned int comp : comps)
if (comp < 1 || size_t(comp) > mixed_final_slot_count ||
mixed_final_slots.count(int(comp) - 1) != 0) {
mix_error = "components reference missing slots";
mix_error = L("components reference missing slots");
break;
}
}
if (!mix_error.empty()) {
skipped_keys.emplace_back("material:" + material_label + " (mixed filament definition: " + mix_error + ")");
skipped_keys.emplace_back("material:" + material_label + " (" +
(boost::format(L("mixed filament definition: %1%")) % mix_error).str() + ")");
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": published 3MF mixed filament from slot " << entry.slot
<< " rejected: " << mix_error;
// Degradation, not silent corruption: the slot was already grown and
@@ -6713,9 +7208,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// is reported as skipped above.
if (!this->is_mixed_filament(slot)) {
finalize_mixed_placeholder(slot);
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
": mixed filament definition could not be imported (" +
mix_error + ")");
published_config->material_replacements.emplace_back(
(boost::format(L("slot %1%: mixed filament definition could not be imported (%2%)")) % slot % mix_error).str());
}
continue;
}
@@ -6786,8 +7280,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
const std::string old_name = this->filament_presets[slot];
this->filament_presets[slot] = new_name;
material_applied = true;
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) + ": " + old_name + " -> " +
new_name);
published_config->material_replacements.emplace_back(
(boost::format(L("slot %1%: %2% -> %3%")) % slot % old_name % new_name).str());
// Colour is slot-scoped and project-visible: sync into project_config
// (the copy already baked it, this makes the chips render).
if (entry.publish_color && !entry.color.empty()) {
@@ -6906,11 +7400,12 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
<< " (score " << score << ", preset_name \"" << entry.preset_name << "\", filament_id \""
<< entry.filament_id << "\", setting_id \"" << entry.setting_id << "\", type \""
<< entry.publish_type_value << "\")";
std::string replacement_line = "slot " + std::to_string(slot) + ": " + old_name + " -> " + replacement;
// A pick that is not the exact published material is a substitute;
// an entry without identity fields cannot be judged, so it stays plain.
if (score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty()))
replacement_line += " (substitute)";
const bool is_substitute = score < 2 && (!entry.filament_id.empty() || !entry.filament_vendor.empty());
std::string replacement_line = (boost::format(is_substitute ? L("slot %1%: %2% -> %3% (substitute)") :
L("slot %1%: %2% -> %3%")) %
slot % old_name % replacement).str();
published_config->material_replacements.emplace_back(std::move(replacement_line));
} else {
// Partial publish with no replacement: keep the receiver's
@@ -6926,8 +7421,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
// mix is left untouched).
if (is_mixed_definition(entry) && !this->is_mixed_filament(slot)) {
finalize_mixed_placeholder(slot);
published_config->material_replacements.emplace_back("slot " + std::to_string(slot) +
": mixed filament definition could not be imported");
published_config->material_replacements.emplace_back(
(boost::format(L("slot %1%: mixed filament definition could not be imported")) % slot).str());
}
}
}
@@ -7010,8 +7505,8 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
if (!this->is_mixed_filament(size_t(gap_slot))) {
finalize_mixed_placeholder(size_t(gap_slot));
published_config->material_replacements.emplace_back(
"slot " + std::to_string(gap_slot) +
": unassigned mixed filament (printer supports only " + std::to_string(physical_capacity) + " filaments)");
(boost::format(L("slot %1%: unassigned mixed filament (the printer's filament limit is %2%)")) % gap_slot %
physical_capacity).str());
}
}
}
@@ -7064,7 +7559,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
static ConfigurationError failed_loading_error(const std::string& file, const std::string& dir)
{
return ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % file % dir).str());
return ConfigurationError((boost::format(L("Failed loading configuration file %1%\nSuggest cleaning the directory %2% first")) % file % dir).str());
}
void PresetBundle::log_errors(const std::vector<std::string>& errors)
@@ -7537,7 +8032,8 @@ void PresetBundle::parse_vendor_json(VendorRead& read)
auto config_version = Semver::parse(version_str);
if (! config_version) {
++m_errors;
throw ConfigurationError((boost::format("vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% firstly")
// TRN %1% is the vendor name, %2% the version found in its profile, %3% the profile directory.
throw ConfigurationError((boost::format(L("Vendor %1%'s config version: %2% invalid\nSuggest cleaning the directory %3% first"))
% vendor_name % version_str % dir).str());
} else {
vendor_profile.config_version = std::move(*config_version);
@@ -7575,7 +8071,7 @@ void PresetBundle::parse_vendor_json(VendorRead& read)
}
catch(nlohmann::detail::parse_error &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<<root_file<<" got a nlohmann::detail::parse_error, reason = " << err.what();
throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly")
throw ConfigurationError((boost::format(L("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% first"))
%root_file %err.what() % dir).str());
//goto __error_process;
}
@@ -7690,7 +8186,7 @@ void PresetBundle::parse_vendor_json(VendorRead& read)
}
catch(nlohmann::detail::parse_error &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse "<< subfile <<" got a nlohmann::detail::parse_error, reason = " << err.what();
throw ConfigurationError((boost::format("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% firstly")
throw ConfigurationError((boost::format(L("Failed loading configuration file %1%: %2%\nSuggest cleaning the directory %3% first"))
%subfile %err.what() % dir).str());
}
+63
View File
@@ -302,6 +302,9 @@ public:
// BBS Load user presets
PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule, bool read_only = false);
PresetsConfigSubstitutions load_user_presets(AppConfig &config, std::map<std::string, std::map<std::string, std::string>>& my_presets, ForwardCompatibilitySubstitutionRule rule);
bool reload_local_bundle(const std::string& preset_folder, const std::string& bundle_id, std::string* error = nullptr);
std::vector<std::string> list_local_bundle_ids(const std::string& preset_folder);
// Orca: Import subscribed bundle presets (load and save to disk in one operation), handles one bundle at a time
PresetsConfigSubstitutions update_subscribed_presets(AppConfig& config,
const std::map<std::string, std::map<std::string, std::string>>& bundle_presets,
@@ -376,12 +379,49 @@ public:
bool use_bbl_device_tab();
bool backup_user_folder() const;
// Writes the user preset folder into a zip archive at zip_path, its entries under "user/".
bool backup_user_folder_to_zip(const std::string &zip_path) const;
// User filament presets made against an older release that no longer show on printers their parent
// listed then (resources/info/legacy_compatible_printers.json).
std::vector<LegacyUserFilamentPreset> legacy_user_filament_presets();
// Backs up the user preset folder into a zip in data_dir(), then writes each preset's printers into its file
// under one hold of the user presets lock. Nothing is written when the backup failed (backup empty) or the
// lock could not be taken (locked false); otherwise failed names the presets whose files could not be written.
struct LegacyUserFilamentPresetMigration {
bool locked{false};
std::string backup;
std::vector<std::string> failed;
};
LegacyUserFilamentPresetMigration migrate_legacy_user_filament_presets(const std::vector<LegacyUserFilamentPreset> &legacy_presets);
//BBS: project embedded preset logic
PresetsConfigSubstitutions load_project_embedded_presets(std::vector<Preset*> project_presets, ForwardCompatibilitySubstitutionRule substitution_rule);
std::vector<Preset*> get_current_project_embedded_presets();
void reset_project_embedded_presets();
// What importing a file's settings replaces, so that an import which fails part-way, or whose
// file is then rejected, can be undone without copying every preset.
struct Selection
{
struct Collection
{
std::string selected;
DynamicPrintConfig edited;
// The baseline saved_is_dirty() compares against, which feeds the project dirty state.
DynamicPrintConfig saved;
// By index: an import only inserts project-embedded presets, which a restore drops first.
std::vector<bool> visible;
};
Collection prints, filaments, printers;
std::vector<std::string> filament_presets;
DynamicPrintConfig project_config;
std::vector<std::map<int, int>> extruder_ams_counts;
};
// Nothing while project-embedded presets exist: a restore drops them, which would lose a loaded project's presets.
std::optional<Selection> selection() const;
void restore_selection(const Selection &selection);
//BBS: find printer model
std::string get_texture_for_printer_model(std::string model_name);
std::string get_stl_model_for_printer_model(std::string model_name);
@@ -390,6 +430,21 @@ public:
// Export selections (current print, current filaments, current printer) into config.ini
void export_selections(AppConfig &config);
// While one lives, export_selections() writes nothing, so a selection only shown for a while (the
// settings of a G-code being previewed) is not remembered as the user's. It also covers the
// exports the GUI makes on its own while refreshing.
class SelectionExportBlocker
{
public:
explicit SelectionExportBlocker(PresetBundle &bundle) : m_bundle(bundle) { ++m_bundle.m_selection_export_blockers; }
~SelectionExportBlocker() { --m_bundle.m_selection_export_blockers; }
SelectionExportBlocker(const SelectionExportBlocker &) = delete;
SelectionExportBlocker &operator=(const SelectionExportBlocker &) = delete;
private:
PresetBundle &m_bundle;
};
// n is the total slot count, and growth appends at the raw tail - which is where the mixed
// slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and
// then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does.
@@ -531,6 +586,11 @@ public:
// If the file is loaded successfully, its print / filament / printer profiles will be activated.
ConfigSubstitutions load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule);
// The settings an OrcaSlicer G-code was sliced with, from its config block, ready for load_config_model().
// Throws when the file carries no such block (another slicer, or the block was left out).
DynamicPrintConfig config_from_gcode(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule,
ConfigSubstitutions &substitutions) const;
// Load a config bundle file, into presets and store the loaded presets into separate files
// of the local configuration directory.
// Load settings into the provided settings instance.
@@ -881,6 +941,9 @@ private:
std::string vendor_to_validate = "";
int m_errors = 0;
// The live SelectionExportBlockers.
int m_selection_export_blockers = 0;
// Helper function: save preset to bundle directory with common logic
bool save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection,
const std::string& bundle_id, const std::string& type_subdir,
+106 -110
View File
@@ -149,6 +149,8 @@ void Print::clear()
m_print_regions.clear();
m_model.clear_objects();
m_statistics_by_extruder_count.clear();
m_same_shrinkage_compensations = false;
m_shrinkage_compensation = Vec3d::Ones();
}
bool Print::has_tpu_filament() const
@@ -584,32 +586,36 @@ std::vector<unsigned int> Print::object_extruders() const
for (const PrintRegion &region : object->all_regions())
region.collect_object_printing_extruders(*this, extruders);
for (const PrintObject* object : m_objects) {
const ModelObject* mo = object->model_object();
for (const ModelVolume* mv : mo->volumes) {
std::vector<int> volume_extruders = mv->get_extruders();
for (int extruder : volume_extruders) {
assert(extruder > 0);
extruders.push_back(extruder - 1);
}
}
// layer range
for (auto layer_range : mo->layer_config_ranges) {
if (layer_range.second.has("extruder")) {
//BBS: actually when user doesn't change filament by height range(value is default 0), height range should not save key "extruder".
//Don't know why height range always save key "extruder" because of no change(should only save difference)...
//Add protection here to avoid overflow
auto value = layer_range.second.option("extruder")->getInt();
if (value > 0)
extruders.push_back(value - 1);
}
}
}
for (const PrintObject* object : m_objects)
append_model_object_extruders(*object->model_object(), extruders);
sort_remove_duplicates(extruders);
return extruders;
}
// Appends 0-based indices of the filaments assigned to the volumes of model_object (painting included) and to its height ranges.
void Print::append_model_object_extruders(const ModelObject &mo, std::vector<unsigned int> &extruders)
{
for (const ModelVolume* mv : mo.volumes) {
std::vector<int> volume_extruders = mv->get_extruders();
for (int extruder : volume_extruders) {
assert(extruder > 0);
extruders.push_back(extruder - 1);
}
}
// layer range
for (auto layer_range : mo.layer_config_ranges) {
if (layer_range.second.has("extruder")) {
//BBS: actually when user doesn't change filament by height range(value is default 0), height range should not save key "extruder".
//Don't know why height range always save key "extruder" because of no change(should only save difference)...
//Add protection here to avoid overflow
auto value = layer_range.second.option("extruder")->getInt();
if (value > 0)
extruders.push_back(value - 1);
}
}
}
// returns 0-based indices of used extruders
std::vector<unsigned int> Print::support_material_extruders() const
{
@@ -619,22 +625,8 @@ std::vector<unsigned int> Print::support_material_extruders() const
auto num_extruders = (unsigned int)m_config.filament_diameter.size();
for (PrintObject *object : m_objects) {
if (object->has_support_material()) {
assert(object->config().support_filament >= 0);
if (object->config().support_filament == 0)
support_uses_current_extruder = true;
else {
unsigned int i = (unsigned int)object->config().support_filament - 1;
extruders.emplace_back((i >= num_extruders) ? 0 : i);
}
assert(object->config().support_interface_filament >= 0);
if (object->config().support_interface_filament == 0)
support_uses_current_extruder = true;
else {
unsigned int i = (unsigned int)object->config().support_interface_filament - 1;
extruders.emplace_back((i >= num_extruders) ? 0 : i);
}
}
if (object->has_support_material() && append_support_extruders(object->config(), num_extruders, extruders))
support_uses_current_extruder = true;
}
if (support_uses_current_extruder)
@@ -645,31 +637,61 @@ std::vector<unsigned int> Print::support_material_extruders() const
return extruders;
}
// Appends 0-based indices of the support and support interface filaments of an object printing support material.
// Returns true if support or support interface is printed with the filament of the object.
bool Print::append_support_extruders(const PrintObjectConfig &config, unsigned int num_extruders, std::vector<unsigned int> &extruders)
{
bool uses_current_extruder = false;
assert(config.support_filament >= 0);
if (config.support_filament == 0)
uses_current_extruder = true;
else {
unsigned int i = (unsigned int)config.support_filament - 1;
extruders.emplace_back((i >= num_extruders) ? 0 : i);
}
assert(config.support_interface_filament >= 0);
if (config.support_interface_filament == 0)
uses_current_extruder = true;
else {
unsigned int i = (unsigned int)config.support_interface_filament - 1;
extruders.emplace_back((i >= num_extruders) ? 0 : i);
}
return uses_current_extruder;
}
// returns 0-based indices of used extruders
std::vector<unsigned int> Print::extruders(bool conside_custom_gcode) const
{
std::vector<unsigned int> extruders = this->object_extruders();
append(extruders, this->support_material_extruders());
if (conside_custom_gcode) {
//BBS
int num_extruders = m_config.filament_colour.size();
if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) {
for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) {
if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders)
extruders.push_back((unsigned int)(item.extruder - 1));
}
if (conside_custom_gcode)
this->append_tool_change_extruders(extruders);
this->append_wipe_tower_extruder(extruders);
sort_remove_duplicates(extruders);
return extruders;
}
// Appends 0-based indices of the filaments the tool changes of the current plate switch to.
void Print::append_tool_change_extruders(std::vector<unsigned int> &extruders) const
{
//BBS
int num_extruders = m_config.filament_colour.size();
if (m_model.plates_custom_gcodes.find(m_model.curr_plate_index) != m_model.plates_custom_gcodes.end()) {
for (auto item : m_model.plates_custom_gcodes.at(m_model.curr_plate_index).gcodes) {
if (item.type == CustomGCode::Type::ToolChange && item.extruder <= num_extruders)
extruders.push_back((unsigned int)(item.extruder - 1));
}
}
}
void Print::append_wipe_tower_extruder(std::vector<unsigned int> &extruders) const
{
// If a wipe tower filament is explicitly set, ensure it participates in tool ordering.
if (has_wipe_tower() && config().wipe_tower_filament != 0 && extruders.size() > 1) {
assert(config().wipe_tower_filament > 0 && config().wipe_tower_filament <= int(config().filament_diameter.size()));
extruders.emplace_back(config().wipe_tower_filament - 1); // config value is 1-based
}
sort_remove_duplicates(extruders);
return extruders;
}
unsigned int Print::num_object_instances() const
@@ -1484,7 +1506,7 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
// return {inst->model_instance->get_object()->name + L(" is too close to exclusion area, there may be collisions when printing.") + "\n",
// inst->model_instance->get_object()};
//ORCA: Pass ModelInstance instead of ModelObject
return {inst->model_instance->get_object()->name + L(" is too close to exclusion area, there may be collisions when printing.") + "\n",
return {(boost::format(L("%1% is too close to exclusion area. There may be collisions when printing.")) % inst->model_instance->get_object()->name).str() + "\n",
inst->model_instance};
}
@@ -1492,7 +1514,7 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
// return {inst->model_instance->get_object()->name + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n",
// inst->model_instance->get_object()};
//ORCA: Pass ModelInstance instead of ModelObject
return {inst->model_instance->get_object()->name + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n",
return {(boost::format(L("%1% is too close to the clumping detection area. There may be collisions when printing.")) % inst->model_instance->get_object()->name).str() + "\n",
inst->model_instance};
}
current_instance_hulls.emplace_back(volume_hull);
@@ -1577,20 +1599,20 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
// many setups that print fine.
if (!intersection(convex_hulls_other, convex_hulls_temp).empty()) {
if (warning) {
warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n");
warning->string += L("Prime tower is too close to others, and collisions may be caused.\n");
}
}
if (!intersection(exclude_polys, tower_polys_checked).empty()) {
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")};
return {L("Prime tower is too close to an exclusion area, and collisions will be caused.\n")};
}
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_checked).empty()) {
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
return {L("Prime tower is too close to the clumping detection area, and collisions will be caused.\n")};
}
if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) {
warning->string += L("Prime Tower") + L(" is too close to exclusion area, there may be collisions when printing.") + "\n";
warning->string += L("Prime tower is too close to an exclusion area. There may be collisions when printing.") + "\n";
}
if (warning && print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_estimated).empty()) {
warning->string += L("Prime Tower") + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n";
warning->string += L("Prime tower is too close to the clumping detection area. There may be collisions when printing.") + "\n";
}
// No gate on "is there a tower": one that is not printed estimates to zero, so the hulls
// are degenerate and every check passes. Re-deriving it here missed the wrapping-detection
@@ -1600,9 +1622,9 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
for (Polygon &p : printable_polys)
p.translate(plate_shift);
if (!diff(tower_polys_checked, printable_polys).empty())
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
return {L("Prime tower is partially outside the printable area, and it cannot be printed.\n")};
if (warning && !diff(tower_polys_estimated, printable_polys).empty())
warning->string += L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n");
warning->string += L("Prime tower is partially outside the printable area, and it cannot be printed.\n");
return {};
}
@@ -1969,14 +1991,19 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
// What is left is a printer with more filaments than logical extruders, where
// the tail slots fall outside the map. Narrow, but without this the sentence
// ends in a dangling "on .".
if (routed_list.empty())
routed_list = L("no configured tool");
StringObjectException err;
err.string = Slic3r::format(
L("IDEX/IQEX mode \"%1%\" prints with T%2%, but this plate's filaments are on %3%. "
"Assign a filament loaded on T%2%, or switch this plate to Primary mode."),
parallel_mode, routing.primary_phys, routed_list);
if (routed_list.empty())
err.string = Slic3r::format(
// TRN %1% is the IDEX/IQEX mode name, %2% the tool number.
L("IDEX/IQEX mode \"%1%\" prints with T%2%, but none of this plate's filaments are on a configured tool. "
"Assign a filament loaded on T%2%, or switch this plate to Primary mode."),
parallel_mode, routing.primary_phys);
else
err.string = Slic3r::format(
// TRN %1% is the IDEX/IQEX mode name, %2% the tool number, %3% a list of tools such as "T1, T2".
L("IDEX/IQEX mode \"%1%\" prints with T%2%, but this plate's filaments are on %3%. "
"Assign a filament loaded on T%2%, or switch this plate to Primary mode."),
parallel_mode, routing.primary_phys, routed_list);
// Gives the notification a "Jump to <object>" link, which selects the object
// and switches to Prepare -- directly enabling the first suggested remedy.
err.object = m_objects.front();
@@ -2230,9 +2257,11 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
//FIXME It is quite expensive to generate object layers just to get the print height!
auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx), print_object.config().precise_z_height.value);
if (!layers.empty()) {
effective_max_z = layers.back();
// The object layers start above the raft.
const double raft_z = print_object.slicing_parameters().object_print_z_min;
effective_max_z = raft_z + layers.back();
last_layer_below_max = layers.size() >= 2 &&
0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON;
raft_z + 0.5 * (layers[layers.size() - 2] + layers.back()) <= this->config().printable_height + EPSILON;
have_height = true;
}
}
@@ -2240,7 +2269,11 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (!have_height)
continue;
if (shrinkage_compensation_z != 1. && effective_max_z > (this->config().printable_height / shrinkage_compensation_z + EPSILON)) {
// The layers of a regular printer already carry the Z shrinkage compensation, the belt height does not.
const double compensated_max_z = belt_printer ? effective_max_z * shrinkage_compensation_z : effective_max_z;
const double uncompensated_max_z = belt_printer ? effective_max_z : print_object.slicing_parameters().object_print_z_uncompensated_max;
if (shrinkage_compensation_z != 1. && compensated_max_z > this->config().printable_height + EPSILON &&
uncompensated_max_z <= this->config().printable_height + EPSILON) {
// The object exceeds the maximum build volume height because of shrinkage compensation.
return StringObjectException{
Slic3r::format(_u8L("While the object %1% itself fits the build volume, it exceeds the maximum build volume height because of material shrinkage compensation."), print_object.model_object()->name),
@@ -3458,7 +3491,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
m_tool_ordering = ToolOrdering(*this, -1, false);
m_tool_ordering.sort_and_build_data(*this, -1, false);
if (m_tool_ordering.empty() || m_tool_ordering.last_extruder() == unsigned(-1))
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
throw Slic3r::SlicingError(L("The print is empty. The model is not printable with current print settings."));
}
this->set_done(psWipeTower);
@@ -5407,11 +5440,11 @@ void Print::_make_wipe_tower()
BOOST_LOG_TRIVIAL(error) << boost::format("wipe tower footprint [%1%,%2%]-[%3%,%4%] leaves printable [%5%,%6%]-[%7%,%8%]") %
unscaled(fp.min.x()) % unscaled(fp.min.y()) % unscaled(fp.max.x()) % unscaled(fp.max.y()) %
unscaled(pr.min.x()) % unscaled(pr.min.y()) % unscaled(pr.max.x()) % unscaled(pr.max.y());
throw Slic3r::SlicingError(L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n"));
throw Slic3r::SlicingError(L("Prime tower is partially outside the printable area, and it cannot be printed.\n"));
}
// The cutter/purge corner is a physical obstacle — the brim must stay out like the body.
if (!intersection(get_bed_excluded_area(m_config), Polygons{footprint}).empty())
throw Slic3r::SlicingError(L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n"));
throw Slic3r::SlicingError(L("Prime tower is too close to an exclusion area, and collisions will be caused.\n"));
}
}
@@ -5526,7 +5559,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
fire_lifecycle_event(LifecycleEvent::GCodeExportFinished, ctx);
}
throw Slic3r::RuntimeError(
std::string("Failed to process the G-code file ") + file + " from previous 3mf\n");
// TRN %1% is a file path.
Slic3r::format(L("Failed to process the G-code file %1% from the previous 3MF file."), file) + "\n");
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": process the G-code file %1% successfully")%file.c_str();
@@ -5640,44 +5674,6 @@ std::string PrintStatistics::finalize_output_path(const std::string &path_in) co
return final_path;
}
// Orca: Implement prusa's filament shrink compensation approach
// Returns if all used filaments have same shrinkage compensations.
bool Print::has_same_shrinkage_compensations() const {
const std::vector<unsigned int> extruders = this->extruders();
if (extruders.empty())
return false;
const double filament_shrinkage_compensation_xy = m_config.filament_shrink.get_at(extruders.front());
const double filament_shrinkage_compensation_z = m_config.filament_shrinkage_compensation_z.get_at(extruders.front());
for (unsigned int extruder : extruders) {
if (filament_shrinkage_compensation_xy != m_config.filament_shrink.get_at(extruder) ||
filament_shrinkage_compensation_z != m_config.filament_shrinkage_compensation_z.get_at(extruder)) {
return false;
}
}
return true;
}
// Orca: Implement prusa's filament shrink compensation approach, but amended so 100% from the user is the equivalent to 0 in orca.
// Returns scaling for each axis representing shrinkage compensations in each axis.
Vec3d Print::shrinkage_compensation() const
{
if (!this->has_same_shrinkage_compensations())
return Vec3d::Ones();
const unsigned int first_extruder = this->extruders().front();
const double xy_shrinkage_percent = m_config.filament_shrink.get_at(first_extruder);
const double z_shrinkage_percent = m_config.filament_shrinkage_compensation_z.get_at(first_extruder);
const double xy_compensation = 100.0 / xy_shrinkage_percent;
const double z_compensation = 100.0 / z_shrinkage_percent;
return { xy_compensation, xy_compensation, z_compensation };
}
const std::string PrintStatistics::FilamentUsedG = "filament used [g]";
const std::string PrintStatistics::FilamentUsedGMask = "; filament used [g] =";
+20 -8
View File
@@ -249,9 +249,9 @@ struct PrintInstance
// SoftFever
//
// instance id
size_t id;
size_t id{0};
// Orca: unique id used by marlin/rrf cancel object feature
size_t unique_id;
size_t unique_id{0};
//BBS: instance_shift is too large because of multi-plate, apply without plate offset.
Point shift_without_plate_offset() const;
@@ -503,9 +503,12 @@ public:
std::vector<std::reference_wrapper<const PrintRegion>> all_regions() const;
const PrintObjectRegions* shared_regions() const throw() { return m_shared_regions; }
bool has_support() const { return m_config.enable_support || m_config.enforce_support_layers > 0; }
bool has_raft() const { return m_config.raft_layers > 0; }
bool has_support_material() const { return this->has_support() || this->has_raft(); }
static bool has_support(const PrintObjectConfig &config) { return config.enable_support || config.enforce_support_layers > 0; }
static bool has_raft(const PrintObjectConfig &config) { return config.raft_layers > 0; }
static bool has_support_material(const PrintObjectConfig &config) { return has_support(config) || has_raft(config); }
bool has_support() const { return has_support(m_config); }
bool has_raft() const { return has_raft(m_config); }
bool has_support_material() const { return has_support_material(m_config); }
// Checks if the model object is painted using the multi-material painting gizmo.
bool is_mm_painted() const { return this->model_object()->is_mm_painted(); }
// Checks if the model object is painted using the fuzzy skin painting gizmo.
@@ -735,7 +738,7 @@ private:
// SoftFever
//
// object id
size_t m_id;
size_t m_id{0};
void apply_conical_overhang();
public:
@@ -1385,10 +1388,11 @@ public:
size_t config_index_generation() const { return m_config_index_generation; }
// Orca: Implement prusa's filament shrink compensation approach
// Both are decided by apply() from the filaments the model uses, before the PrintObject transformations are built.
// Returns if all used filaments have same shrinkage compensations.
bool has_same_shrinkage_compensations() const;
bool has_same_shrinkage_compensations() const { return m_same_shrinkage_compensations; }
// Returns scaling for each axis representing shrinkage compensations in each axis.
Vec3d shrinkage_compensation() const;
Vec3d shrinkage_compensation() const { return m_shrinkage_compensation; }
std::tuple<float, float> object_skirt_offset(double margin_height = 0) const;
@@ -1484,11 +1488,19 @@ private:
s_slicing_pipeline_hook_fn(*this, object, step);
}
static void append_model_object_extruders(const ModelObject &mo, std::vector<unsigned int> &extruders);
static bool append_support_extruders(const PrintObjectConfig &config, unsigned int num_extruders, std::vector<unsigned int> &extruders);
void append_tool_change_extruders(std::vector<unsigned int> &extruders) const;
void append_wipe_tower_extruder(std::vector<unsigned int> &extruders) const;
void update_shrinkage_compensation(size_t num_extruders, std::vector<int> &variant_index);
PrintConfig m_config;
PrintObjectConfig m_default_object_config;
PrintRegionConfig m_default_region_config;
PrintObjectPtrs m_objects;
PrintRegionPtrs m_print_regions;
bool m_same_shrinkage_compensations { false };
Vec3d m_shrinkage_compensation { Vec3d::Ones() };
//SoftFever
bool m_isBBLPrinter = false;
+76
View File
@@ -25,6 +25,7 @@
#include <vector>
#include <cstddef>
#include <initializer_list>
#include <iterator>
#include <set>
#include <cstdlib>
#include <functional>
@@ -1257,6 +1258,80 @@ static PrintObjectRegions* generate_print_object_regions(
return out.release();
}
// Orca: The shrinkage compensation is baked into the PrintObject transformations, so it has to be known before apply()
// creates them. Derive it from the filaments the new model prints with instead of from the existing PrintObjects and
// their regions, which do not exist yet on the first apply(). Every feature filament of every part, height range and
// modifier counts, whether the feature ends up printing or not, so filaments that disagree switch the compensation
// off rather than letting a wrong compensation through.
void Print::update_shrinkage_compensation(size_t num_extruders, std::vector<int> &variant_index)
{
const auto set_compensation = [this](unsigned int extruder) {
// 100% from the user is the equivalent to 0 in orca.
const double xy_compensation = 100.0 / m_config.filament_shrink.get_at(extruder);
const double z_compensation = 100.0 / m_config.filament_shrinkage_compensation_z.get_at(extruder);
m_shrinkage_compensation = { xy_compensation, xy_compensation, z_compensation };
};
const auto same_shrinkage = [this](unsigned int a, unsigned int b) {
return m_config.filament_shrink.get_at(a) == m_config.filament_shrink.get_at(b) &&
m_config.filament_shrinkage_compensation_z.get_at(a) == m_config.filament_shrinkage_compensation_z.get_at(b);
};
// Usually all filaments share the same shrinkage, then it does not matter which of them print.
bool uniform = true;
for (unsigned int extruder = 1; extruder < num_extruders && uniform; ++ extruder)
uniform = same_shrinkage(0, extruder);
if (uniform) {
m_same_shrinkage_compensations = true;
set_compensation(0);
return;
}
std::vector<unsigned int> extruders;
const auto append_feature_extruders = [num_extruders, &extruders](const PrintRegionConfig &config) {
for (int id : { config.outer_wall_filament_id.value, config.inner_wall_filament_id.value, config.sparse_infill_filament_id.value,
config.internal_solid_filament_id.value, config.top_surface_filament_id.value, config.bottom_surface_filament_id.value }) {
const unsigned int i = (unsigned int)std::max(0, id - 1);
extruders.emplace_back(i >= num_extruders ? 0 : i);
}
};
for (const ModelObject *model_object : m_model.objects) {
if (std::none_of(model_object->instances.begin(), model_object->instances.end(), [](const ModelInstance *mi) { return mi->is_printable(); }))
continue;
std::vector<const ModelVolume*> modifiers;
std::copy_if(model_object->volumes.begin(), model_object->volumes.end(), std::back_inserter(modifiers), [](const ModelVolume *mv) { return mv->is_modifier(); });
for (const ModelVolume *part : model_object->volumes) {
if (! part->is_model_part())
continue;
std::vector<PrintRegionConfig> parents { region_config_from_model_volume(m_default_region_config, nullptr, *part, num_extruders, variant_index) };
for (const auto &[range, range_config] : model_object->layer_config_ranges)
parents.emplace_back(region_config_from_model_volume(m_default_region_config, &range_config.get(), *part, num_extruders, variant_index));
for (const PrintRegionConfig &parent : parents) {
append_feature_extruders(parent);
for (const ModelVolume *modifier : modifiers)
append_feature_extruders(region_config_from_model_volume(parent, nullptr, *modifier, num_extruders, variant_index));
}
}
append_model_object_extruders(*model_object, extruders);
const PrintObjectConfig object_config = PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders, variant_index);
if (PrintObject::has_support_material(object_config))
append_support_extruders(object_config, (unsigned int)num_extruders, extruders);
}
this->append_tool_change_extruders(extruders);
// A mixed slot prints with its physical components.
const auto expand_and_sort = [this, &extruders]() {
extruders = expand_mixed_filaments(extruders, m_config.filament_is_mixed.values, m_config.filament_mixed_components.values);
};
expand_and_sort();
// The wipe tower filament may also be flushed into the objects.
this->append_wipe_tower_extruder(extruders);
expand_and_sort();
m_same_shrinkage_compensations = ! extruders.empty() &&
std::all_of(extruders.begin(), extruders.end(), [&](unsigned int extruder) { return same_shrinkage(extruders.front(), extruder); });
m_shrinkage_compensation = Vec3d::Ones();
if (m_same_shrinkage_compensations)
set_compensation(extruders.front());
}
Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_config, bool extruder_applied)
{
#ifdef _DEBUG
@@ -1864,6 +1939,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// 4) Generate PrintObjects from ModelObjects and their instances.
{
this->update_shrinkage_compensation(num_extruders, print_variant_index);
PrintObjectPtrs print_objects_new;
print_objects_new.reserve(std::max(m_objects.size(), m_model.objects.size()));
bool new_objects = false;
+138 -124
View File
@@ -1656,7 +1656,7 @@ void PrintConfigDef::init_fff_params()
def->tooltip = L("Line width of the Bridge. If expressed as a %, it will be computed over the nozzle diameter.\n"
"Recommended to use with a higher Bridge density or Bridge flow ratio.\n\n"
"The maximum value is 100% or the nozzle diameter.\n"
"If set to 0, the line width will match the Internal solid infill width.");
"If set to 0, the line width will match the Internal solid infill width, or the nozzle diameter for thick bridges.");
def->sidetext = L("mm or %");
def->ratio_over = "nozzle_diameter";
def->min = 0;
@@ -1919,7 +1919,7 @@ void PrintConfigDef::init_fff_params()
"3. If artifacts still appear, enable Extrusion Rate Smoothing (ERS) to further smooth the flow transitions.\n\n"
"Note: When this option is enabled, overhang perimeters are treated like overhangs, meaning the overhang speed is "
"applied even if the overhanging perimeter is part of a bridge.\n"
"For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 100% overhang speed will be applied.");
"For example, when the perimeters are 100% overhanging, with no wall supporting them from underneath, the 75% overhang speed will be applied, limited by the bridge speed.");
def->mode = comAdvanced;
def->nullable = true;
def->set_default_value(new ConfigOptionBoolsNullable{ false });
@@ -1980,7 +1980,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("External");
def->category = L("Speed");
def->tooltip = L("Speed of the externally visible bridge extrusions.\n\n"
"In addition, if Slow down for curled perimeters is disabled or Classic overhang mode is enabled, "
"In addition, if Slow down for curled perimeters is disabled, "
"it will be the print speed of overhang walls that are supported by less than 13%, "
"whether they are part of a bridge or an overhang.");
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
@@ -2438,7 +2438,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("machine_end_gcode", coString);
def->label = L("End G-code");
def->tooltip = L("Add end G-Code when finishing the entire print.");
def->tooltip = L("Add end G-code when finishing the entire print.");
def->multiline = true;
def->full_width = true;
def->height = 12;
@@ -2650,7 +2650,7 @@ void PrintConfigDef::init_fff_params()
def->category = L("Speed");
def->tooltip = L("This separate setting will affect the speed of perimeters having radius <= small_perimeter_threshold "
"(usually holes). If expressed as percentage (for example: 80%) it will be calculated "
"on the outer wall speed setting above. Set to zero for auto.");
"on the outer wall speed setting above.");
def->sidetext = L("mm/s or %");
def->ratio_over = "outer_wall_speed";
def->min = 1;
@@ -2673,10 +2673,9 @@ void PrintConfigDef::init_fff_params()
def->category = L("Speed");
def->tooltip = L("Same as \"Small perimeters\", but for supports. "
"This separate setting will affect the speed of support for areas <= `small_support_perimeter_threshold`. "
"If expressed as a percentage (for example: 80%), it will be calculated on the support or support interface speed setting above. "
"Set to zero for auto.");
"If expressed as a percentage (for example: 80%), it will be calculated on the support or support interface speed setting above.");
def->sidetext = L("mm/s or %");
def->ratio_over = "outer_wall_speed";
def->ratio_over = "support_speed";
def->min = 1;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloatsOrPercentsNullable{FloatOrPercent(50, true)});
@@ -2700,7 +2699,7 @@ void PrintConfigDef::init_fff_params()
"However, overhang performance will reduce as there is no internal perimeter to print the external wall against. "
"This option requires a minimum of 3 walls to be effective as it prints the internal walls from the 3rd perimeter onwards first, "
"then the external perimeter and, finally, the first internal perimeter. "
"This option is recommended against the Outer/Inner option in most cases.\n\n"
"This option is recommended over the Outer/Inner option in most cases.\n\n"
"Use Outer/Inner for the same external wall quality and dimensional accuracy benefits of Inner/Outer/Inner option. "
"However, the Z seams will appear less consistent as the first extrusion of a new layer starts on a visible surface.");
def->enum_keys_map = &ConfigOptionEnum<WallSequence>::get_enum_values();
@@ -4012,7 +4011,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("initial_layer_speed", coFloats);
def->label = L("First layer");
def->tooltip = L("This is the speed for the first layer except for solid infill sections.");
def->tooltip = L("This is the speed for the walls and brim of the first layer.");
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
def->min = 1;
def->mode = comAdvanced;
@@ -4021,7 +4020,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("initial_layer_infill_speed", coFloats);
def->label = L("First layer infill");
def->tooltip = L("This is the speed for solid infill parts of the first layer.");
def->tooltip = L("This is the speed for the first layer except for walls and brim, such as infill, gap fill and support.");
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
def->min = 1;
def->mode = comAdvanced;
@@ -4578,7 +4577,7 @@ void PrintConfigDef::init_fff_params()
//def->enum_values.push_back("machinekit");
//def->enum_values.push_back("smoothie");
//def->enum_values.push_back("no-extrusion");
def->enum_labels.push_back("Marlin(legacy)");
def->enum_labels.push_back(L("Marlin (legacy)"));
def->enum_labels.push_back(L("Klipper"));
def->enum_labels.push_back("RepRapFirmware");
def->enum_labels.push_back("Repetier");
@@ -4935,7 +4934,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Infill/wall overlap");
def->category = L("Strength");
// xgettext:no-c-format, no-boost-format
def->tooltip = L("This allows the infill area to be enlarged slightly to overlap with walls for better bonding. The percentage value is relative to line width of sparse infill. Set this value to ~10-15% to minimize potential over extrusion and accumulation of material resulting in rough top surfaces.");
def->tooltip = L("This allows the infill area to be enlarged slightly to overlap with walls for better bonding. The percentage value is relative to the inner wall line width. Set this value to ~10-15% to minimize potential over extrusion and accumulation of material resulting in rough top surfaces.");
def->sidetext = "%";
def->ratio_over = "inner_wall_line_width";
def->mode = comAdvanced;
@@ -4948,7 +4947,7 @@ void PrintConfigDef::init_fff_params()
def->tooltip = L("Top solid infill area is enlarged slightly to overlap with wall for better bonding "
"and to minimize the appearance of pinholes where the top infill meets the walls. "
"A value of 25-30% is a good starting point, minimizing the appearance of pinholes. "
"The percentage value is relative to line width of sparse infill.");
"The percentage value is relative to the inner wall line width.");
def->sidetext = "%";
def->ratio_over = "inner_wall_line_width";
def->mode = comAdvanced;
@@ -4998,9 +4997,9 @@ void PrintConfigDef::init_fff_params()
def = this->add("mmu_segmented_region_interlocking_depth", coFloat);
def->label = L("Interlocking depth of a segmented region");
def->tooltip = L("Interlocking depth of a segmented region. It will be ignored if "
"\"mmu_segmented_region_max_width\" is zero or if \"mmu_segmented_region_interlocking_depth\" "
"is bigger than \"mmu_segmented_region_max_width\". Zero disables this feature.");
def->tooltip = L("Interlocking depth of a segmented region. On every other layer, the segmented region "
"is limited to this depth. It will be ignored if \"interlocking_beam\" is enabled. "
"Zero disables this feature.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->min = 0;
def->category = L("Advanced");
@@ -5168,7 +5167,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Minimize wall height angle");
def->category = L("Quality");
def->tooltip = L("Reduce the height of top-surface perimeters to match the model edge height.\n"
"Affects perimeters with a slope less than this angle (degrees).\n"
"Affects perimeters with a slope greater than this angle (degrees).\n"
"A reasonable value is 35. Set to 0 to disable.");
def->sidetext = u8"°"; // degrees, don't need translation
def->min = 0;
@@ -5301,11 +5300,11 @@ void PrintConfigDef::init_fff_params()
(void)L("Maximum speed E");
def->category = L("Machine limits");
def->readonly = false;
def->tooltip = (boost::format("Maximum speed of %1% axis") % axis_upper).str();
(void)L("Maximum X speed");
(void)L("Maximum Y speed");
(void)L("Maximum Z speed");
(void)L("Maximum E speed");
def->tooltip = (boost::format("Maximum speed of the %1% axis") % axis_upper).str();
(void)L("Maximum speed of the X axis");
(void)L("Maximum speed of the Y axis");
(void)L("Maximum speed of the Z axis");
(void)L("Maximum speed of the E axis");
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
def->min = 0;
def->mode = comSimple;
@@ -5919,7 +5918,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("slicing_pipeline_plugin", coStrings);
def->label = L("Slicing Pipeline Plugin");
def->tooltip = L("Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data, "
def->tooltip = L("Python plugins invoked at each slicing pipeline step to read and modify intermediate slicing data, "
"including a final G-code post-processing step. Research/experimental.");
def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
def->plugin_type = "slicing-pipeline";
@@ -6560,7 +6559,7 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionBool(false));
def = this->add("role_based_wipe_speed", coBool);
def->label = L("Role base wipe speed");
def->label = L("Role based wipe speed");
def->category = L("Speed");
def->tooltip = L("The wipe speed is determined by the speed of the current extrusion role. "
"e.g. if a wipe action is executed immediately following an outer wall extrusion, the speed of the outer wall extrusion will be utilized for the wipe action.");
@@ -6664,9 +6663,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Draft shield");
def->tooltip = L("A draft shield is useful to protect an ABS or ASA print from warping and detaching from print bed due to wind draft. "
"It is usually needed only with open frame printers, i.e. without an enclosure.\n\n"
"Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt height' is used.\n"
"Note: With the draft shield active, the skirt will be printed at skirt distance from the object. "
"Therefore, if brims are active it may intersect with them. To avoid this, increase the skirt distance value.\n");
"Enabled = skirt is as tall as the highest printed object. Otherwise 'Skirt height' is used.");
def->enum_keys_map = &ConfigOptionEnum<DraftShield>::get_enum_values();
def->enum_values.push_back("disabled");
def->enum_values.push_back("enabled");
@@ -6943,7 +6940,7 @@ void PrintConfigDef::init_fff_params()
// are unchanged; only the gcode emission frame shifts.
def = this->add("imex_firmware_managed_zones", coBool);
def->label = L("Firmware-managed zones");
def->tooltip = L("When enabled, the slicer emits g-code with the primary zone's "
def->tooltip = L("When enabled, the slicer emits G-code with the primary zone's "
"contents centered on bed origin (0,0). The printer firmware is "
"responsible for placing copies/mirrors at each physical zone "
"position. Enable only on printers whose firmware applies its own "
@@ -7036,7 +7033,8 @@ void PrintConfigDef::init_fff_params()
def = this->add("imex_head_filament_map", coString);
def->label = L("IDEX/IQEX head filament map");
def->tooltip = L("Per-plate override mapping physical heads to filament slots "
"(1-based). Empty means use pem-inversion defaults.");
"(1-based). Empty means each head uses the first filament that the "
"printer's physical extruder map routes to it.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionString(""));
@@ -7628,7 +7626,7 @@ void PrintConfigDef::init_fff_params()
def->category = L("Printable space");
def->tooltip = L("Axis the mesh is rotated about before slicing. This is the belt "
"printer's tilt: an isometric (no distortion) rotation that also "
"drives bed rendering and support gravity tilt, and that the g-code "
"drives bed rendering and support gravity tilt, and that the G-code "
"back-transform inverts before the machine-frame shear/scale and remap. "
"X is the typical gantry tilt (belt travels along Y).");
def->enum_keys_map = &ConfigOptionEnum<BeltRotationAxis>::get_enum_values();
@@ -7653,7 +7651,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("belt_frame_tilt_decouple", coBool);
def->label = L("Decouple machine-frame tilt");
def->category = L("Printable space");
def->tooltip = L("Expert override: set the machine-frame (g-code shear/scale) tilt angle "
def->tooltip = L("Expert override: set the machine-frame (G-code shear/scale) tilt angle "
"independently of the pre-slice rotation angle. When disabled, the "
"machine-frame transform is derived from the belt tilt angle, so a single "
"angle drives both stages. Enable only to compensate for a machine whose "
@@ -8273,10 +8271,10 @@ void PrintConfigDef::init_fff_params()
def->label = L("Maximum wipe tower print speed");
def->tooltip = L("The maximum print speed when purging in the wipe tower and printing the wipe tower sparse layers. "
"When purging, if the sparse infill speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n\n"
"When printing the sparse layers, if the internal perimeter speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n\n"
"When printing the sparse layers, if the sparse infill speed or calculated speed from the filament max volumetric speed is lower, the lowest will be used instead.\n\n"
"Increasing this speed may affect the tower's stability as well as increase the force with which the nozzle collides with any blobs that may have formed on the wipe tower.\n\n"
"Before increasing this parameter beyond the default of 90 mm/s, make sure your printer can reliably bridge at the increased speeds and that ooze when tool changing is well controlled.\n\n"
"For the wipe tower external perimeters the internal perimeter speed is used regardless of this setting.");
"For the wipe tower external perimeters the internal perimeter speed is used, also limited by this setting.");
def->sidetext = L("mm/s"); // millimeters per second, CIS languages need translation
def->mode = comAdvanced;
def->min = 10;
@@ -8285,9 +8283,9 @@ void PrintConfigDef::init_fff_params()
def = this->add("wipe_tower_wall_type", coEnum);
def->label = L("Wall type");
def->tooltip = L("Wipe tower outer wall type.\n"
"1. Rectangle: The default wall type, a rectangle with fixed width and height.\n"
"1. Rectangle: A rectangle with fixed width and height.\n"
"2. Cone: A cone with a fillet at the bottom to help stabilize the wipe tower.\n"
"3. Rib: Adds four ribs to the tower wall for enhanced stability.");
"3. Rib: The default wall type, which adds four ribs to the tower wall for enhanced stability.");
def->enum_keys_map = &ConfigOptionEnum<WipeTowerWallType>::get_enum_values();
def->enum_values.emplace_back("rectangle");
def->enum_values.emplace_back("cone");
@@ -8418,7 +8416,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("wipe_tower_extra_flow", coPercent);
def->label = L("Extra flow for purging");
def->tooltip = L("Extra flow used for the purging lines on the wipe tower. This makes the purging lines thicker or narrower "
def->tooltip = L("Extra flow used for the purging lines on the wipe tower. This makes the purging lines thicker "
"than they normally would be. The spacing is adjusted automatically.");
def->sidetext = "%";
def->mode = comAdvanced;
@@ -8595,11 +8593,10 @@ void PrintConfigDef::init_fff_params()
def->label = L("Minimum wall length");
def->category = L("Quality");
def->tooltip = L("Adjust this value to prevent short, unclosed walls from being printed, which could increase print time. "
"Higher values remove more and longer walls.\n\n"
"The value is a multiple of the wall width. Higher values remove more and longer walls.\n\n"
"NOTE: Bottom and top surfaces will not be affected by this value to prevent visual gaps on the outside of the model. "
"Adjust 'One wall threshold' in the Advanced settings below to adjust the sensitivity of what is considered a top-surface. "
"Adjust 'One wall threshold' in the 'Walls and surfaces' settings below to adjust the sensitivity of what is considered a top-surface. "
"'One wall threshold' is only visible if this setting is set above the default value of 0.5, or if single-wall top surfaces is enabled.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->mode = comAdvanced;
def->min = 0.0;
def->max = 25.0;
@@ -8620,7 +8617,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Maximum wall deviation");
def->category = L("Quality");
def->tooltip = L("The maximum deviation allowed when reducing the resolution for the 'Maximum wall resolution' setting. If you increase this, "
"the print will be less accurate, but the G-Code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', "
"the print will be less accurate, but the G-code will be smaller. 'Maximum wall deviation' limits 'Maximum wall resolution', "
"so if the two conflict, 'Maximum wall deviation' takes precedence.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->mode = comExpert;
@@ -9939,7 +9936,8 @@ void PrintConfigDef::handle_legacy_composite(DynamicPrintConfig &config)
auto [thumbnails_list, errors] = GCodeThumbnails::make_and_check_thumbnail_list(thumbnails_str, extention);
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = "\n" + Slic3r::format("Invalid value provided for parameter %1%: %2%", "thumbnails", thumbnails_str);
// TRN %1% is a parameter name, %2% its value.
std::string error_str = "\n" + Slic3r::format(_u8L("Invalid value provided for parameter %1%: %2%"), "thumbnails", thumbnails_str);
error_str += GCodeThumbnails::get_error_string(errors);
throw BadOptionValueException(error_str);
}
@@ -12328,42 +12326,42 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
std::map<std::string, std::string> error_message;
// --layer-height
if (cfg.get_abs_value("layer_height") <= 0) {
error_message.emplace("layer_height", L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
error_message.emplace("layer_height", _u8L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
}
else if (fabs(fmod(cfg.get_abs_value("layer_height"), SCALING_FACTOR)) > 1e-4) {
error_message.emplace("layer_height", L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
error_message.emplace("layer_height", _u8L("invalid value ") + std::to_string(cfg.get_abs_value("layer_height")));
}
// --first-layer-height
if (cfg.initial_layer_print_height.value <= 0) {
error_message.emplace("initial_layer_print_height", L("invalid value ") + std::to_string(cfg.initial_layer_print_height.value));
error_message.emplace("initial_layer_print_height", _u8L("invalid value ") + std::to_string(cfg.initial_layer_print_height.value));
}
// --filament-diameter
for (double fd : cfg.filament_diameter.values)
if (fd < 1) {
error_message.emplace("filament_diameter", L("invalid value ") + cfg.filament_diameter.serialize());
error_message.emplace("filament_diameter", _u8L("invalid value ") + cfg.filament_diameter.serialize());
break;
}
// --nozzle-diameter
for (double nd : cfg.nozzle_diameter.values)
if (nd < 0.005) {
error_message.emplace("nozzle_diameter", L("invalid value ") + cfg.nozzle_diameter.serialize());
error_message.emplace("nozzle_diameter", _u8L("invalid value ") + cfg.nozzle_diameter.serialize());
break;
}
// --perimeters
if (cfg.wall_loops.value < 0) {
error_message.emplace("wall_loops", L("invalid value ") + std::to_string(cfg.wall_loops.value));
error_message.emplace("wall_loops", _u8L("invalid value ") + std::to_string(cfg.wall_loops.value));
}
// --solid-layers
if (cfg.top_shell_layers < 0) {
error_message.emplace("top_shell_layers", L("invalid value ") + std::to_string(cfg.top_shell_layers));
error_message.emplace("top_shell_layers", _u8L("invalid value ") + std::to_string(cfg.top_shell_layers));
}
if (cfg.bottom_shell_layers < 0) {
error_message.emplace("bottom_shell_layers", L("invalid value ") + std::to_string(cfg.bottom_shell_layers));
error_message.emplace("bottom_shell_layers", _u8L("invalid value ") + std::to_string(cfg.bottom_shell_layers));
}
if (cfg.use_firmware_retraction.value &&
@@ -12384,61 +12382,61 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
// --gcode-flavor
if (! print_config_def.get("gcode_flavor")->has_enum_value(cfg.gcode_flavor.serialize())) {
error_message.emplace("gcode_flavor", L("invalid value ") + cfg.gcode_flavor.serialize());
error_message.emplace("gcode_flavor", _u8L("invalid value ") + cfg.gcode_flavor.serialize());
}
// --fill-pattern
if (! print_config_def.get("sparse_infill_pattern")->has_enum_value(cfg.sparse_infill_pattern.serialize())) {
error_message.emplace("sparse_infill_pattern", L("invalid value ") + cfg.sparse_infill_pattern.serialize());
error_message.emplace("sparse_infill_pattern", _u8L("invalid value ") + cfg.sparse_infill_pattern.serialize());
}
// --top-fill-pattern
if (! print_config_def.get("top_surface_pattern")->has_enum_value(cfg.top_surface_pattern.serialize())) {
error_message.emplace("top_surface_pattern", L("invalid value ") + cfg.top_surface_pattern.serialize());
error_message.emplace("top_surface_pattern", _u8L("invalid value ") + cfg.top_surface_pattern.serialize());
}
// --bottom-fill-pattern
if (! print_config_def.get("bottom_surface_pattern")->has_enum_value(cfg.bottom_surface_pattern.serialize())) {
error_message.emplace("bottom_surface_pattern", L("invalid value ") + cfg.bottom_surface_pattern.serialize());
error_message.emplace("bottom_surface_pattern", _u8L("invalid value ") + cfg.bottom_surface_pattern.serialize());
}
// --soild-fill-pattern
if (!print_config_def.get("internal_solid_infill_pattern")->has_enum_value(cfg.internal_solid_infill_pattern.serialize())) {
error_message.emplace("internal_solid_infill_pattern", L("invalid value ") + cfg.internal_solid_infill_pattern.serialize());
error_message.emplace("internal_solid_infill_pattern", _u8L("invalid value ") + cfg.internal_solid_infill_pattern.serialize());
}
// --skirt-height
if (cfg.skirt_height < 0) {
error_message.emplace("skirt_height", L("invalid value ") + std::to_string(cfg.skirt_height));
error_message.emplace("skirt_height", _u8L("invalid value ") + std::to_string(cfg.skirt_height));
}
// --bridge-flow-ratio
if (cfg.bridge_flow <= 0) {
error_message.emplace("bridge_flow", L("invalid value ") + std::to_string(cfg.bridge_flow));
error_message.emplace("bridge_flow", _u8L("invalid value ") + std::to_string(cfg.bridge_flow));
}
// --internal-bridge-flow-ratio
if (cfg.internal_bridge_flow <= 0) {
error_message.emplace("internal_bridge_flow", L("invalid value ") + std::to_string(cfg.internal_bridge_flow));
error_message.emplace("internal_bridge_flow", _u8L("invalid value ") + std::to_string(cfg.internal_bridge_flow));
}
// extruder clearance
if (cfg.extruder_clearance_radius <= 0) {
error_message.emplace("extruder_clearance_radius", L("invalid value ") + std::to_string(cfg.extruder_clearance_radius));
error_message.emplace("extruder_clearance_radius", _u8L("invalid value ") + std::to_string(cfg.extruder_clearance_radius));
}
if (cfg.extruder_clearance_height_to_rod <= 0) {
error_message.emplace("extruder_clearance_height_to_rod", L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_rod));
error_message.emplace("extruder_clearance_height_to_rod", _u8L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_rod));
}
if (cfg.extruder_clearance_height_to_lid <= 0) {
error_message.emplace("extruder_clearance_height_to_lid", L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_lid));
error_message.emplace("extruder_clearance_height_to_lid", _u8L("invalid value ") + std::to_string(cfg.extruder_clearance_height_to_lid));
}
if (cfg.nozzle_height <= 0)
error_message.emplace("nozzle_height", L("invalid value ") + std::to_string(cfg.nozzle_height));
error_message.emplace("nozzle_height", _u8L("invalid value ") + std::to_string(cfg.nozzle_height));
// --extrusion-multiplier
for (double em : cfg.filament_flow_ratio.values)
if (em <= 0) {
error_message.emplace("filament_flow_ratio", L("invalid value ") + cfg.filament_flow_ratio.serialize());
error_message.emplace("filament_flow_ratio", _u8L("invalid value ") + cfg.filament_flow_ratio.serialize());
break;
}
@@ -12448,27 +12446,27 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
// Note that we might want to have more than one perimeter on the bottom
// solid layers.
if (cfg.wall_loops != 1) {
error_message.emplace("wall_loops", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.wall_loops));
error_message.emplace("wall_loops", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.wall_loops));
//return "Can't make more than one perimeter when spiral vase mode is enabled";
//return "Can't make less than one perimeter when spiral vase mode is enabled";
}
if (cfg.sparse_infill_density > 0) {
error_message.emplace("sparse_infill_density", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.sparse_infill_density));
error_message.emplace("sparse_infill_density", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.sparse_infill_density));
//return "Spiral vase mode can only print hollow objects, so you need to set Fill density to 0";
}
if (cfg.top_shell_layers > 0) {
error_message.emplace("top_shell_layers", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.top_shell_layers));
error_message.emplace("top_shell_layers", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.top_shell_layers));
//return "Spiral vase mode is not compatible with top solid layers";
}
if (cfg.enable_support ) {
error_message.emplace("enable_support", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enable_support));
error_message.emplace("enable_support", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enable_support));
//return "Spiral vase mode is not compatible with support";
}
if (cfg.enforce_support_layers > 0) {
error_message.emplace("enforce_support_layers", L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enforce_support_layers));
error_message.emplace("enforce_support_layers", _u8L("Invalid value when spiral vase mode is enabled: ") + std::to_string(cfg.enforce_support_layers));
//return "Spiral vase mode is not compatible with support";
}
}
@@ -12499,9 +12497,9 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
double allowed_max = (key == "bridge_line_width") ? min_nozzle_diameter : MAX_LINE_WIDTH_MULTIPLIER * max_nozzle_diameter;
if (abs_width > allowed_max) {
if (key == "bridge_line_width")
error_message.emplace(key, L("Bridge line width must not exceed nozzle diameter: ") + std::to_string(abs_width));
error_message.emplace(key, _u8L("Bridge line width must not exceed nozzle diameter: ") + std::to_string(abs_width));
else
error_message.emplace(key, L("too large line width ") + std::to_string(abs_width));
error_message.emplace(key, _u8L("too large line width ") + std::to_string(abs_width));
//return std::string("Too Large line width: ") + key;
}
}
@@ -12548,7 +12546,8 @@ std::map<std::string, std::string> validate(const FullPrintConfig &cfg, bool und
}
if (out_of_range) {
if (error_message.find(opt_key) == error_message.end())
error_message.emplace(opt_key, opt->serialize() + L(" not in range ") +"[" + std::to_string(optdef->min) + "," + std::to_string(optdef->max) + "]");
// TRN %1% is the current value, %2% and %3% the lowest and highest allowed values.
error_message.emplace(opt_key, Slic3r::format(_u8L("%1% not in range [%2%, %3%]"), opt->serialize(), optdef->min, optdef->max));
//return std::string("Value out of range: " + opt_key);
}
}
@@ -12606,7 +12605,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
// Actions:
/*def = this->add("export_obj", coBool);
def->label = L("Export OBJ");
def->tooltip = L("Export the model(s) as OBJ.");
def->tooltip = L("Export the models as OBJ.");
def->set_default_value(new ConfigOptionBool(false));*/
/*
@@ -12642,7 +12641,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
/*def = this->add("export_amf", coBool);
def->label = L("Export AMF");
def->tooltip = L("Export the model(s) as AMF.");
def->tooltip = L("Export the models as AMF.");
def->set_default_value(new ConfigOptionBool(false));*/
def = this->add("export_stl", coBool);
@@ -12926,7 +12925,7 @@ CLITransformConfigDef::CLITransformConfigDef()
/*def = this->add("split", coBool);
def->label = L("Split");
def->tooltip = L("Detect unconnected parts in the given model(s) and split them into separate objects.");
def->tooltip = L("Detect unconnected parts in the given models and split them into separate objects.");
def = this->add("scale_to_fit", coPoint3);
def->label = L("Scale to Fit");
@@ -13041,6 +13040,13 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def->cli_params = "dir";
def->set_default_value(new ConfigOptionString());
def = this->add("export_statistics", coBool);
def->label = L("Export statistics");
def->tooltip = L("Write the statistics of each sliced plate as JSON, as plate_N.statistics.json next to its G-code in the output "
"directory and inside the 3MF of --export-3mf: print time, filament and feature totals, and the same per layer "
"and per object.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("debug", coInt);
def->label = L("Debug level");
def->tooltip = L("Sets debug logging level. 0:fatal, 1:error, 2:warning, 3:info, 4:debug, 5:trace\n");
@@ -13234,12 +13240,12 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef()
def->label = L("Has single extruder MM priming");
def->tooltip = L("Are the extra multi-material priming regions used in this print?");
new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool.");
new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone.");
new_def("curr_bed_type", coString, "Current bed type", "Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate').");
new_def("imex_mode", coString, "IDEX/IQEX active mode", "Name of the active IDEX/IQEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IDEX/IQEX is not enabled.");
new_def("imex_mode_index", coInt, "IDEX/IQEX active mode index", "Zero-based index of the active IDEX/IQEX parallel print mode within imex_mode_names.");
new_def("imex_mode_gcode", coString, "IDEX/IQEX active mode G-code", "The raw mode G-code template for the active IDEX/IQEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode.");
new_def("initial_no_support_extruder", coInt, L("Initial no support extruder"), L("Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool."));
new_def("in_head_wrap_detect_zone", coBool, L("In head wrap detect zone"), L("Indicates if the first layer overlaps with the head wrap zone."));
new_def("curr_bed_type", coString, L("Current bed type"), L("Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate')."));
new_def("imex_mode", coString, L("IDEX/IQEX active mode"), L("Name of the active IDEX/IQEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IDEX/IQEX is not enabled."));
new_def("imex_mode_index", coInt, L("IDEX/IQEX active mode index"), L("Zero-based index of the active IDEX/IQEX parallel print mode within imex_mode_names."));
new_def("imex_mode_gcode", coString, L("IDEX/IQEX active mode G-code"), L("The raw mode G-code template for the active IDEX/IQEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode."));
}
PrintStatisticsConfigDef::PrintStatisticsConfigDef()
@@ -13357,8 +13363,8 @@ ObjectsInfoConfigDef::ObjectsInfoConfigDef()
def->label = L("Input filename without extension");
def->tooltip = L("Source filename of the first object, without extension.");
new_def("input_filename", coString, "Full input filename", "Source filename of the first object.");
new_def("plate_name", coString, "Plate name", "Name of the plate sliced.");
new_def("input_filename", coString, L("Full input filename"), L("Source filename of the first object."));
new_def("plate_name", coString, L("Plate name"), L("Name of the plate sliced."));
}
DimensionsConfigDef::DimensionsConfigDef()
@@ -13397,22 +13403,22 @@ DimensionsConfigDef::DimensionsConfigDef()
def->label = L("Size of the print bed bounding box");
def->tooltip = bb_size_tooltip;
new_def("first_layer_center_no_wipe_tower", coFloats, "First layer center without wipe tower", point_tooltip);
new_def("first_layer_height", coFloat, "First layer height", "Height of the first layer.");
new_def("first_layer_center_no_wipe_tower", coFloats, L("First layer center without wipe tower"), point_tooltip);
new_def("first_layer_height", coFloat, L("First layer height"), L("Height of the first layer."));
}
TemperaturesConfigDef::TemperaturesConfigDef()
{
ConfigOptionDef* def;
new_def("bed_temperature", coInts, "Bed temperature", "Vector of bed temperatures for each extruder/filament.")
new_def("bed_temperature_initial_layer", coInts, "First layer bed temperature", "Vector of first layer bed temperatures for each extruder/filament. Provides the same value as first_layer_bed_temperature.")
new_def("bed_temperature_initial_layer_single", coInt, "First layer bed temperature (initial extruder)", "First layer bed temperature for the initial extruder. Same as bed_temperature_initial_layer[initial_extruder]")
new_def("chamber_temperature", coInts, "Chamber temperature", "Vector of chamber temperatures for each extruder/filament.")
new_def("chamber_minimal_temperature", coInts, "Chamber minimal temperature", "Vector of minimal chamber temperatures for each extruder/filament.")
new_def("overall_chamber_temperature", coInt, "Overall chamber temperature", "Overall chamber temperature. This value is the maximum chamber temperature of any extruder/filament used.")
new_def("first_layer_bed_temperature", coInts, "First layer bed temperature", "Vector of first layer bed temperatures for each extruder/filament. Provides the same value as bed_temperature_initial_layer.")
new_def("first_layer_temperature", coInts, "First layer temperature", "Vector of first layer temperatures for each extruder/filament.")
new_def("bed_temperature", coInts, L("Bed temperature"), L("Vector of bed temperatures for each extruder/filament."))
new_def("bed_temperature_initial_layer", coInts, L("First layer bed temperature"), L("Vector of first layer bed temperatures for each extruder/filament. Provides the same value as first_layer_bed_temperature."))
new_def("bed_temperature_initial_layer_single", coInt, L("First layer bed temperature (initial extruder)"), L("First layer bed temperature for the initial extruder. Same as bed_temperature_initial_layer[initial_extruder]"))
new_def("chamber_temperature", coInts, L("Chamber temperature"), L("Vector of chamber temperatures for each extruder/filament."))
new_def("chamber_minimal_temperature", coInts, L("Chamber minimal temperature"), L("Vector of minimal chamber temperatures for each extruder/filament."))
new_def("overall_chamber_temperature", coInt, L("Overall chamber temperature"), L("Overall chamber temperature. This value is the maximum chamber temperature of any extruder/filament used."))
new_def("first_layer_bed_temperature", coInts, L("First layer bed temperature"), L("Vector of first layer bed temperatures for each extruder/filament. Provides the same value as bed_temperature_initial_layer."))
new_def("first_layer_temperature", coInts, L("First layer temperature"), L("Vector of first layer temperatures for each extruder/filament."))
}
@@ -13519,42 +13525,50 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
def->tooltip = L("The current extruder ID. The same as current_extruder.");
// change_filament_gcode
new_def("previous_extruder", coInt, "Previous extruder", "Index of the extruder that is being unloaded. The index is zero based (first extruder has index 0).");
new_def("next_extruder", coInt, "Next extruder", "Index of the extruder that is being loaded. The index is zero based (first extruder has index 0).");
new_def("relative_e_axis", coBool, "Relative e-axis", "Indicates if relative positioning is being used.");
new_def("toolchange_count", coInt, "Toolchange count", "The number of toolchanges throught the print.");
new_def("previous_extruder", coInt, L("Previous extruder"), L("Index of the extruder that is being unloaded. The index is zero based (first extruder has index 0)."));
new_def("next_extruder", coInt, L("Next extruder"), L("Index of the extruder that is being loaded. The index is zero based (first extruder has index 0)."));
new_def("relative_e_axis", coBool, L("Relative e-axis"), L("Indicates if relative positioning is being used."));
new_def("toolchange_count", coInt, L("Toolchange count"), L("The number of toolchanges throughout the print."));
new_def("fan_speed", coNone, "", ""); //Option is no longer used and is zeroed by placeholder parser for compatability
new_def("old_retract_length", coFloat, "Old retract length", "The retraction length of the previous filament.");
new_def("new_retract_length", coFloat, "New retract length", "The retraction lenght of the new filament.");
new_def("old_retract_length_toolchange", coFloat, "Old retract length toolchange", "The toolchange retraction length of the previous filament.");
new_def("new_retract_length_toolchange", coFloat, "New retract length toolchange", "The toolchange retraction length of the new filament.");
new_def("old_filament_temp", coInt, "Old filament temp", "The old filament temp.");
new_def("new_filament_temp", coInt, "New filament temp", "The new filament temp.");
new_def("x_after_toolchange", coFloat, "X after toolchange", "The X pos after toolchange.");
new_def("y_after_toolchange", coFloat, "Y after toolchange", "The Y pos after toolchange.");
new_def("z_after_toolchange", coFloat, "Z after toolchange", "The Z pos after toolchange.");
new_def("first_flush_volume", coFloat, "First flush volume", "The first flush volume.");
new_def("second_flush_volume", coFloat, "Second flush volume", "The second flush volume.");
new_def("old_filament_e_feedrate", coInt, "Old filament e feedrate", "The old filament extruder feedrate.");
new_def("new_filament_e_feedrate", coInt, "New filament e feedrate", "The new filament extruder feedrate.");
new_def("travel_point_1_x", coFloat, "Travel point 1 X", "The travel point 1 X.");
new_def("travel_point_1_y", coFloat, "Travel point 1 Y", "The travel point 1 Y.");
new_def("travel_point_2_x", coFloat, "Travel point 2 X", "The travel point 2 X.");
new_def("travel_point_2_y", coFloat, "Travel point 2 Y", "The travel point 2 Y.");
new_def("travel_point_3_x", coFloat, "Travel point 3 X", "The travel point 3 X.");
new_def("travel_point_3_y", coFloat, "Travel point 3 Y", "The travel point 3 Y.");
new_def("flush_length_1", coFloat, "Flush Length 1", "The first flush length.");
new_def("flush_length_2", coFloat, "Flush Length 2", "The second flush length.");
new_def("flush_length_3", coFloat, "Flush Length 3", "The third flush length.");
new_def("flush_length_4", coFloat, "Flush Length 4", "The fourth flush length.");
new_def("old_retract_length", coFloat, L("Old retract length"), L("The retraction length of the previous filament."));
new_def("new_retract_length", coFloat, L("New retract length"), L("The retraction length of the new filament."));
new_def("old_retract_length_toolchange", coFloat, L("Old retract length toolchange"), L("The toolchange retraction length of the previous filament."));
new_def("new_retract_length_toolchange", coFloat, L("New retract length toolchange"), L("The toolchange retraction length of the new filament."));
new_def("old_filament_temp", coInt, L("Old filament temp"), L("The old filament temp."));
new_def("new_filament_temp", coInt, L("New filament temp"), L("The new filament temp."));
new_def("x_after_toolchange", coFloat, L("X after toolchange"), L("The X pos after toolchange."));
new_def("y_after_toolchange", coFloat, L("Y after toolchange"), L("The Y pos after toolchange."));
new_def("z_after_toolchange", coFloat, L("Z after toolchange"), L("The Z pos after toolchange."));
new_def("first_flush_volume", coFloat, L("First flush volume"), L("The first flush volume."));
new_def("second_flush_volume", coFloat, L("Second flush volume"), L("The second flush volume."));
new_def("old_filament_e_feedrate", coInt, L("Old filament e feedrate"), L("The old filament extruder feedrate."));
new_def("new_filament_e_feedrate", coInt, L("New filament e feedrate"), L("The new filament extruder feedrate."));
new_def("travel_point_1_x", coFloat, L("Travel point 1 X"), L("The travel point 1 X."));
new_def("travel_point_1_y", coFloat, L("Travel point 1 Y"), L("The travel point 1 Y."));
new_def("travel_point_2_x", coFloat, L("Travel point 2 X"), L("The travel point 2 X."));
new_def("travel_point_2_y", coFloat, L("Travel point 2 Y"), L("The travel point 2 Y."));
new_def("travel_point_3_x", coFloat, L("Travel point 3 X"), L("The travel point 3 X."));
new_def("travel_point_3_y", coFloat, L("Travel point 3 Y"), L("The travel point 3 Y."));
new_def("flush_length_1", coFloat, L("Flush Length 1"), L("The first flush length."));
new_def("flush_length_2", coFloat, L("Flush Length 2"), L("The second flush length."));
new_def("flush_length_3", coFloat, L("Flush Length 3"), L("The third flush length."));
new_def("flush_length_4", coFloat, L("Flush Length 4"), L("The fourth flush length."));
// change_extrusion_role_gcode
std::string extrusion_role_types = "Possible Values:\n[\"Perimeter\", \"ExternalPerimeter\", "
"\"OverhangPerimeter\", \"InternalInfill\", \"SolidInfill\", \"TopSolidInfill\", \"BottomSurface\", \"BridgeInfill\", \"GapFill\", \"Ironing\", "
"\"Skirt\", \"Brim\", \"SupportMaterial\", \"SupportMaterialInterface\", \"SupportTransition\", \"WipeTower\", \"Mixed\"]";
// The whole tooltip is one literal so that the GUI can look it up in the catalog.
def = this->add("extrusion_role", coString);
def->label = L("Extrusion role");
// TRN The quoted values are G-code identifiers; keep them in English.
def->tooltip = L("The new extrusion role/type that is going to be used\nPossible values:\n[\"Perimeter\", \"ExternalPerimeter\", "
"\"OverhangPerimeter\", \"InternalInfill\", \"SolidInfill\", \"TopSolidInfill\", \"BottomSurface\", \"BridgeInfill\", \"GapFill\", \"Ironing\", "
"\"Skirt\", \"Brim\", \"SupportMaterial\", \"SupportMaterialInterface\", \"SupportTransition\", \"WipeTower\", \"Mixed\"]");
new_def("extrusion_role", coString, "Extrusion role", "The new extrusion role/type that is going to be used\n" + extrusion_role_types);
new_def("last_extrusion_role", coString, "Last extrusion role", "The previously used extrusion role/type\nPossible Values:\n" + extrusion_role_types);
def = this->add("last_extrusion_role", coString);
def->label = L("Last extrusion role");
// TRN The quoted values are G-code identifiers; keep them in English.
def->tooltip = L("The previously used extrusion role/type\nPossible values:\n[\"Perimeter\", \"ExternalPerimeter\", "
"\"OverhangPerimeter\", \"InternalInfill\", \"SolidInfill\", \"TopSolidInfill\", \"BottomSurface\", \"BridgeInfill\", \"GapFill\", \"Ironing\", "
"\"Skirt\", \"Brim\", \"SupportMaterial\", \"SupportMaterialInterface\", \"SupportTransition\", \"WipeTower\", \"Mixed\"]");
}
const CustomGcodeSpecificConfigDef custom_gcode_specific_config_def;
+7 -7
View File
@@ -885,7 +885,8 @@ void PrintObject::contour_z()
std::scoped_lock lock(mtx);
completed++;
std::string msg = (boost::format("Z contoured layer %d/%d (%d%%)") % (completed) % m_layers.size() % int(double(completed) / m_layers.size() * 100)).str();
// TRN Slicing status: layers done, total layers, percentage done.
std::string msg = (boost::format(L("Z contoured layer %d/%d (%d%%)")) % (completed) % m_layers.size() % int(double(completed) / m_layers.size() * 100)).str();
m_print->set_status(40, msg);
}
}
@@ -958,12 +959,11 @@ void PrintObject::generate_support_material(bool with_belt_brim)
BOOST_LOG_TRIVIAL(info) << std::fixed << std::setprecision(0) << "is_support_necessary takes " << duration << " secs.";
if (sntype != NoNeedSupp) {
std::map<SupportNecessaryType, std::string> reasons = {
{SharpTail,L("floating regions")},
{Cantilever,L("floating cantilever")},
{LargeOverhang,L("large overhangs")} };
std::string warning_message = Slic3r::format(L("It seems object %s has %s. Please re-orient the object or enable support generation."),
this->model_object()->name, reasons[sntype]);
std::map<SupportNecessaryType, std::string> messages = {
{SharpTail,L("It seems object %1% has floating regions. Please re-orient the object or enable support generation.")},
{Cantilever,L("It seems object %1% has a floating cantilever. Please re-orient the object or enable support generation.")},
{LargeOverhang,L("It seems object %1% has large overhangs. Please re-orient the object or enable support generation.")} };
std::string warning_message = Slic3r::format(messages[sntype], this->model_object()->name);
this->active_step_add_warning(PrintStateBase::WarningLevel::NON_CRITICAL, warning_message, PrintStateBase::SlicingNeedSupportOn);
}
+1 -1
View File
@@ -956,7 +956,7 @@ void PrintObject::slice()
}
});
if (m_layers.empty())
throw Slic3r::SlicingError(L("No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.\n"));
throw Slic3r::SlicingError(L("No layers were detected. You might want to repair the model or check its size or thickness and retry.\n"));
// Belt printer: offset all layer Z values so objects at different positions
// along the belt print at different heights on the tilted belt. This is a
File diff suppressed because it is too large. Load diff
+205 -8
View File
@@ -86,6 +86,17 @@ enum class TextureProjectionMethod : int
// that is inherent to view projection and is the user's call, not a bug. The projector's two
// in-plane axes live in TextureDisplacementLayer::view_project_right/up.
ViewProjected = 4,
// The model's own texture coordinates, as the file it was imported from carried them (see
// ModelVolume::texture_uv). Nothing is unwrapped or projected: the artist's layout *is* the
// placement, so a texture authored against that layout - the model's own albedo map, say - lands
// exactly where it was painted. Automatically picked for a new layer on a volume that has a UV
// channel, since re-unwrapping a model that already carries one can only be worse than using it.
//
// The uv is already normalized, so unlike every method above it is not measured in millimetres:
// "Tile size" and the non-square aspect correction do not apply to it (the layer's rotation and
// offset still do). Falls back to Triplanar when the volume has no channel, exactly as LSCM falls
// back when a patch cannot be flattened.
ModelUV = 5,
};
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
@@ -121,6 +132,11 @@ enum class TextureBlendMode : int
Subtract = 1, // acc - value (carve this layer's relief out of the layers below)
Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer)
Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor)
// acc = value: this layer replaces what is under it instead of combining with it, so where it is
// painted only its own relief remains. The image editor's "Normal" mode, under the name the panel
// uses. Note the stack's *colour* already behaves this way - a higher slot's colour overwrites a
// lower one's where they overlap - so this is what makes the displacement agree with it.
Override = 4,
};
// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`.
@@ -154,6 +170,38 @@ struct TextureIsland
// chart to its true mm area, so a freshly unwrapped patch has uniform texel density to begin with.
void average_island_scales(std::vector<TextureIsland> &islands);
// Photo-style adjustments to a layer's source image, as the gizmo's image-adjustment panel sets them.
//
// These are *not* layer parameters: nothing here is stored on TextureDisplacementLayer and nothing here is
// re-applied at bake time. The panel applies them once, to the image, and the result becomes the layer's
// image - so the adjusted pixels feed the colour and the displacement together, and a brightness change
// really does change the relief. That is the intended behaviour: one image, one source of truth, rather
// than a second set of knobs the bake would have to replay in the right order.
//
// Contrast with TextureDisplacementLayer::smoothing, which *is* a layer parameter applied on every decode:
// `blur` here is a destructive edit of the image, `smoothing` is a live low-pass over whatever the image is.
struct TextureAdjustments
{
float brightness = 0.f; // [-1, 1]; 0 unchanged, -1 black, +1 white
float contrast = 0.f; // [-1, 1]; 0 unchanged, -1 flat mid-grey, +1 hard threshold
float sharpness = 0.f; // [0, 1]; 0 unchanged. Unsharp-mask amount
float blur = 0.f; // [0, 1]; 0 unchanged. Fraction of SOURCE_BLUR_SPAN, as `smoothing` is
float hue_deg = 0.f; // [-180, 180]; 0 unchanged. No effect on a grayscale image - grey has no hue
bool identity() const
{
return brightness == 0.f && contrast == 0.f && sharpness == 0.f && blur == 0.f && hue_deg == 0.f;
}
// Exact equality, which is what is wanted: these come straight off sliders, and the question asked of
// them is "is this still the value the preview on screen was computed from", not "are these close".
bool operator==(const TextureAdjustments &o) const
{
return brightness == o.brightness && contrast == o.contrast && sharpness == o.sharpness &&
blur == o.blur && hue_deg == o.hue_deg;
}
bool operator!=(const TextureAdjustments &o) const { return !(*this == o); }
};
// One texture asset plus its projection/displacement parameters. Several layers may be painted
// onto overlapping areas of the same volume: their displacements are combined per vertex, in slot
// order, each layer folding into the total via its own TextureBlendMode (see
@@ -277,6 +325,18 @@ struct TextureDisplacementLayer
// - zero or positive: a mesh vertex, every unwrapped copy of it (how edits were stored before).
std::vector<std::pair<int, Vec2f>> lscm_uv_overrides;
// Live, unsaved image adjustments from the gizmo's image-adjustment panel, applied by
// decode_height_texture() on top of the decoded image - so while the panel is open the relief, the
// colour, the thumbnails and the bake all see the adjusted image, and moving a slider really does
// change the displacement. See TextureAdjustments.
//
// Deliberately *not* serialized, by either save()/load() here or the .3mf: it is the state of an open
// editor, not a property of the layer. The panel resets it when it closes, and "Save as copy" resolves
// it by writing a new image and pointing the layer at that - which is the only way an adjustment
// becomes permanent. A project saved mid-adjustment therefore stores the unadjusted image, which is
// the honest answer: nothing was committed.
TextureAdjustments preview_adjust;
// How this layer folds into the displacement accumulated by the layers below it. Ignored for
// the lowest-slot painted layer, which has nothing beneath it to combine with (the GUI shows
// it as the "Base" layer and hides the control).
@@ -504,6 +564,36 @@ struct DecodedHeightTexture
bool texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method, TexelTap &out) const;
};
// Applies `adj` to `src` and returns the result; `src` is left alone. An empty `src`, or an identity
// `adj`, gives back a copy.
//
// Order is tonal, then spatial: brightness, contrast and hue per texel first, then blur, then sharpness.
// Sharpness last is what lets the two spatial knobs be used together - blur to kill speckle, then
// sharpness to put the real edges back - instead of the blur simply undoing the sharpen.
//
// Grayscale in, grayscale out (`rgb` stays empty): promoting a grey image to RGB would hand the layer's
// colour feature a row of identical greys to "colour" the model with, and triple the stored PNG for it.
// For a colour image the height is recomputed as the luminance of the *adjusted* colour, with
// decode_height_texture()'s own coefficients, so the relief and the colour can never drift apart.
DecodedHeightTexture apply_texture_adjustments(const DecodedHeightTexture &src, const TextureAdjustments &adj);
// Blur radius, in texels, that TextureAdjustments::blur == 1 means on a `min(width, height)`-sized image:
// the same 5%-of-the-short-side span the Smoothing slider uses, under the same 48-texel cost cap, so the
// two controls bite at the same rate and a user moving between them is not surprised.
float adjustment_blur_radius(float blur, int width, int height);
// Box-filters `src` down so neither side exceeds `max_px`, preserving aspect; never upscales, and
// returns a copy when it already fits. Colour and height are filtered the same way, and the height
// stays the height - it is not recomputed from the filtered colour, because a box filter is linear and
// luminance is linear in the channels, so the two agree already.
//
// This is what lets the adjustment panel's live preview cost depend on the size of the box it is drawn
// in rather than on the size of the image: adjusting a downscaled copy per slider frame, and the
// full-size image only once, on commit. A box filter (rather than a plain decimation) is the point -
// these textures are high-frequency by nature, and dropping texels would alias the preview into
// showing detail the adjustment is not actually producing.
DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, int max_px);
// Decode a layer's raw image bytes into sampleable height data, plus colour when the source has any.
// Both 8-bit grayscale PNGs (the shipped library, and anything imported before colour was kept) and
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
@@ -551,11 +641,24 @@ struct TextureColorSettings
bool empty() const { return palette.empty(); }
};
// The two world directions the box projection takes its u and v along on a face whose outward
// normal is `normal`: the plane is the one that normal is closest to, and the pair is oriented as a
// viewer standing outside the model sees it - u to their right, v *down* the image, which is the
// direction v runs in the texture (v = 0 is the image's first pixel row, see
// DecodedHeightTexture::sample()). So an image comes out the way it looks in a file manager rather
// than upside down, and it is not mirrored on the three faces facing the other way.
//
// Exposed because the GUI needs the same basis for the on-canvas "adjust texture placement" gizmo
// (its ring's zero angle and its offset handle's plane), and it must not be a second, drifting copy
// of the table.
void box_projection_axes(const Vec3f &normal, Vec3f &u_axis, Vec3f &v_axis);
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
// layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather
// than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas
// "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space
// tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic.
// layer), dropping the axis position that best aligns with `normal`. Equivalent to projecting onto
// box_projection_axes()' pair. Exposed on its own (rather than only inline inside
// project_texture_displacement_uv()) so GUI code - the on-canvas "adjust texture placement" gizmo -
// can map a dragged 3D point into the exact same 2D space tiling_scale/rotation_deg/offset operate
// in, without duplicating the axis-selection logic.
Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
@@ -568,7 +671,13 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
// square case and leaves the coordinate exactly as it always was.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
//
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
// the image's proportions) applies, and only the rotation and offset are left.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
bool normalized_uv = false);
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
@@ -605,6 +714,31 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr);
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
//
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
// simplification - they leave the surface where it was - and useless after a bake, which moves the
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
// is.
//
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
// on its own.
//
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
// carry, which is not the same as "carry nothing".
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh);
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
@@ -718,6 +852,55 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map.
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
// Three UVs per patch triangle, read out of the model's *own* UV channel - the
// TextureProjectionMethod::ModelUV counterpart of compute_lscm_corner_uvs(), and laid out the same way
// (corner k of triangle i at index 3i+k).
//
// `model_uv` is the volume's channel: three uvs per triangle of `base_mesh`, in its triangle order
// (see ModelVolume::texture_uv). `patch_source` maps each patch triangle back to the base-mesh
// triangle it was cut from - TriangleSelector::get_facets_strict()'s out_source. A patch triangle is
// always a piece of exactly one base triangle, so each of its corners is interpolated barycentrically
// inside that triangle's three uvs. That is what keeps a sub-triangle the brush split (whose corners
// are not base-mesh vertices at all) on the texture, and what keeps the two sides of a UV seam
// reading from their own island instead of from one averaged coordinate.
//
// Returns an empty vector when the channel does not match `base_mesh`, when `patch_source` does not
// match `patch`, or when either is empty - the callers treat that as "no placement" and fall back,
// exactly as they do for an unwrap that failed.
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
const std::vector<Vec2f> &model_uv,
const indexed_triangle_set &patch,
const std::vector<int> &patch_source);
// One UV per patch vertex, collapsed from a per-corner array: the first corner to reach a vertex wins.
// This is the per-corner/per-vertex split compute_lscm_uvs() documents, applied to any corner array -
// displacement is per-vertex, so a vertex on a seam has to settle on one of its copies.
// Returns an empty vector if `corner` is not three per patch triangle.
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner);
// Resamples a per-corner UV channel from `src_mesh` onto `dst_mesh`, for when something replaces a
// volume's mesh and the model's own UV channel has to come along: the colour import's painted mesh,
// the gizmo's remesh/subdivide, the default bake pipeline's rebuild. Each destination corner takes the
// uv of its closest point on the source surface, interpolated barycentrically inside the source
// triangle it lands on - the same transfer LscmLookup does per point, run over every corner.
//
// Exact wherever the two meshes describe the same surface, which is the case for every caller here
// (they subdivide, repair or displace it, they do not reshape it). A corner that lands nowhere near
// the source - farther than `max_distance` (negative: no limit) - takes (0, 0).
//
// One thing a positional transfer cannot reproduce: a *seam* vertex, where one position carries a
// different uv in each island that meets there. Position alone does not say which island a corner
// belongs to, so corners on a seam all take whichever island the nearest-triangle lookup lands on,
// and the texture can be off by up to one triangle in a band along the island borders. Doing better
// needs a triangle-to-triangle correspondence, which only some of the stages that replace a mesh
// have (a remesh has none at all).
//
// Returns an empty vector when `src_uv` is not three per source triangle, or when either mesh is empty.
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
const std::vector<Vec2f> &src_uv,
const indexed_triangle_set &dst_mesh,
float max_distance = -1.f);
// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs).
inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); }
@@ -776,6 +959,10 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
//
// `color`, when given, also reports which filament each triangle should print in - see
// TextureColorRequest.
//
// `model_uv`, when given, is the volume's own UV channel (three uvs per base-mesh triangle, see
// ModelVolume::texture_uv) and is what a TextureProjectionMethod::ModelUV layer maps through. A layer
// set to ModelUV without it falls back to Triplanar.
struct TextureColorRequest
{
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
@@ -850,7 +1037,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
// stage, already brought back into `base_mesh`'s frame.
BakeStageRecorder *debug = nullptr,
// When given, receives what the bake spent.
TextureBakeStats *stats = nullptr);
TextureBakeStats *stats = nullptr,
// The model's own UV channel; see above.
const std::vector<Vec2f> *model_uv = nullptr);
// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own.
// The mesh is shared by every instance, so a multi-instance object can only be baked for one of
@@ -925,11 +1114,15 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize,
ColorQuantizeFn quantize_pure = nullptr);
ColorQuantizeFn quantize_pure = nullptr,
// The model's own UV channel, for ModelUV layers; see
// build_texture_displacement().
const std::vector<Vec2f> *model_uv = nullptr);
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data);
const TextureDisplacementFacetsData &facets_data,
const std::vector<Vec2f> *model_uv = nullptr);
// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a
// shared cache so a midpoint is computed once and reused by both triangles on either side of that
@@ -1134,6 +1327,10 @@ struct TextureDisplacementPrepareResult
{
indexed_triangle_set mesh;
TextureDisplacementFacetsData masks;
// The model's own UV channel, resampled onto `mesh` (see transfer_uv_channel()). Empty when the
// volume had none. Like the masks it is handed back rather than left to the caller to recompute,
// because only the run itself knows which stages replaced the mesh.
std::vector<Vec2f> uv;
// The remesh landed but no layer's paint survived being carried onto it. Nothing is committed:
// everything downstream is driven by that paint, so baking on would bake a flat mesh.
bool paint_lost = false;
+21
View File
@@ -25,6 +25,7 @@
#include "TextureToColor/ColorUtils.hpp"
#include "Model.hpp"
#include "TextureDisplacement.hpp"
#include "TextureToColor/TriMesh.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
@@ -554,9 +555,29 @@ bool apply_painted_mesh_to_volume(
<< ", unit_scale=" << unit_scale << ")";
}
// The model's own UV channel is indexed by triangle, and this replaces the triangles, so it has
// to be resampled onto them. Kept rather than dropped because it is the layout the model was
// authored against: a texture-displacement layer set to ModelUV maps through it, and the colour
// import is exactly the path a model that carries one arrives by.
const indexed_triangle_set old_its = volume.has_texture_uv() ? volume.mesh().its : indexed_triangle_set{};
const std::vector<Vec2f> old_uv = volume.texture_uv;
volume.set_mesh(std::move(new_mesh));
volume.calculate_convex_hull();
// Resampled here and not further down, while the two meshes still share a frame: the painted mesh
// was just shifted by the same source.mesh_offset the old one was centred by, so the only thing
// between them is whatever drift the repair introduced. The re-centering below translates the mesh
// again, and a translation is exactly what a closest-point transfer cannot see through - it would
// read every uv off a point that far away. A uv is a coordinate in the texture, so it does not care
// about either shift; it just has to be read before the second one happens.
if (!old_its.indices.empty())
volume.texture_uv = transfer_uv_channel(old_its, old_uv, volume.mesh().its);
else
// No channel to carry, so make sure none is left behind either: a leftover of the right length
// would read as a valid channel against a mesh it was never measured on.
volume.reset_texture_uv();
// Re-center the replaced mesh so its bbox center sits at the origin,
// matching what center_geometry_after_creation did for the original mesh.
// CGAL repair / subdivision may shift the bbox center (drift); without
+1
View File
@@ -66,6 +66,7 @@
#define CLI_OBJECT_ORIENT_FAILED -22
#define CLI_MODIFIED_PARAMS_TO_PRINTER -23
#define CLI_FILE_VERSION_NOT_SUPPORTED -24
#define CLI_EXPORT_STATISTICS_ERROR -25
#define CLI_NO_SUITABLE_OBJECTS -50
+1 -1
View File
@@ -263,7 +263,7 @@ std::string MZ_Archive::get_errorstr(mz_zip_error mz_err)
break;
}
return "unknown error";
return L("unknown error");
}
} // namespace Slic3r
+1 -1
View File
@@ -1948,7 +1948,7 @@ void copy_directory_recursively(const boost::filesystem::path& source,
if (cfr != CopyFileResult::SUCCESS) {
BOOST_LOG_TRIVIAL(error) << "Copying failed(" << cfr << "): " << error_message;
throw Slic3r::CriticalException(Slic3r::format(
("Copying directory %1% to %2% failed: %3%"),
_u8L("Copying directory %1% to %2% failed: %3%"),
source, target, error_message));
}
}
+6
View File
@@ -20,6 +20,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Widgets/FilamentLoad.hpp
GUI/AboutDialog.cpp
GUI/AboutDialog.hpp
GUI/AnimatedTexture.cpp
GUI/AnimatedTexture.hpp
GUI/AmsMappingPopup.cpp
GUI/AmsMappingPopupUpdate.cpp
GUI/AmsMappingPopup.hpp
@@ -127,6 +129,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Shortcuts.hpp
GUI/SpeedDialDialog.cpp
GUI/SpeedDialDialog.hpp
GUI/ColorPickerDialog.cpp
GUI/ColorPickerDialog.hpp
GUI/ActionRegistry.cpp
GUI/ActionRegistry.hpp
GUI/NativeCommands.cpp
@@ -174,6 +178,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoAssembly.hpp
GUI/Gizmos/GLGizmoBase.cpp
GUI/Gizmos/GLGizmoBase.hpp
GUI/Gizmos/GLGizmoAlignment.cpp
GUI/Gizmos/GLGizmoAlignment.hpp
GUI/Gizmos/GLGizmoBrimEars.cpp
GUI/Gizmos/GLGizmoBrimEars.hpp
GUI/Gizmos/GLGizmoCut.cpp
+3
View File
@@ -297,6 +297,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
, printable(true)
, visible(true)
, is_active(true)
, on_hidden_plate(false)
, zoom_to_volumes(true)
, shader_outside_printer_detection_enabled(false)
, is_outside(false)
@@ -824,6 +825,8 @@ bool GLVolume::is_below_printbed() const
void GLVolume::render_sinking_contours()
{
if (!is_active)
return;
m_sinking_contours.render();
}
+2
View File
@@ -211,6 +211,8 @@ public:
bool visible : 1;
// Whether or not this volume is active for rendering
bool is_active : 1;
// Whether or not is_active was cleared because the other plates are hidden
bool on_hidden_plate : 1;
// Whether or not to use this volume when applying zoom_to_volumes()
bool zoom_to_volumes : 1;
// Wheter or not this volume is enabled for outside print volume detection in shader.
+1 -1
View File
@@ -513,7 +513,7 @@ wxBoxSizer* AMSDryCtrWin::create_normal_state_panel(wxPanel* parent)
m_time_input->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
m_time_input->SetForegroundColour(StateColor::darkModeColorFor(*wxBLACK));
Label* time_unit_label = new Label(parent, "H");
Label* time_unit_label = new Label(parent, _L_CONTEXT("H", "Hours unit"));
time_unit_label->SetForegroundColour(*wxBLACK);
time_sizer->Add(m_time_input, 1, wxRIGHT, FromDIP(1));
time_sizer->Add(time_unit_label, 0, wxALIGN_CENTER_VERTICAL);
+30 -44
View File
@@ -1,4 +1,9 @@
#include "AMSMaterialsSetting.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <optional>
#include <variant>
#include <cstdint>
#include "ExtrusionCalibration.hpp"
#include "MsgDialog.hpp"
#include "GUI_App.hpp"
@@ -36,11 +41,9 @@
#include <cmath>
#include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include "slic3r/GUI/Widgets/StaticBox.hpp"
#include <wx/colordlg.h>
#include <wx/dcclient.h>
#include <wx/dc.h>
#include <wx/colour.h>
#include <wx/colourdata.h>
#include <wx/dcgraph.h>
#include <wx/event.h>
#include <wx/gdicmn.h>
@@ -1963,11 +1966,6 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
sizer_custom->Add(m_ts_stbitmap_custom, 0, wxEXPAND, 0);
m_custom_cp->Layout();
m_clrData = new wxColourData();
m_clrData->SetChooseFull(true);
m_clrData->SetChooseAlpha(false);
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
@@ -1991,46 +1989,34 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
wxGetApp().UpdateDarkUIWin(this);
}
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& event)
void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
{
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
for (int i = 0; i < colors.size(); i++) {
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
const bool was_shown = IsShown();
std::optional<wxColour> picker_color;
{
const ColorRGBA initial(m_def_col.Red(), m_def_col.Green(), m_def_col.Blue(), static_cast<unsigned char>(255));
// Snapshot the custom control's anchor before releasing the transient popup grab.
ColorPickerDialog dialog(GetParent(), initial, {}, false, m_custom_cp);
Dismiss();
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
const auto& result = std::get<ColorRGBA>(*dialog.selection());
picker_color = wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
static_cast<unsigned char>(std::lround(result.b() * 255.f)));
}
} // Destroy the modal before restoring the first-level popup and its focus/grab.
if (!picker_color) {
if (was_shown) Popup();
return;
}
auto clr_dialog = new wxColourDialog(nullptr, m_clrData);
wxColour picker_color;
if (clr_dialog->ShowModal() == wxID_OK) {
m_clrData = &(clr_dialog->GetColourData());
if (colors.size() != CUSTOM_COLOR_COUNT) {
colors.resize(CUSTOM_COLOR_COUNT);
}
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
}
wxGetApp().app_config->save_custom_color_to_config(colors);
picker_color = wxColour(
m_clrData->GetColour().Red(),
m_clrData->GetColour().Green(),
m_clrData->GetColour().Blue(),
255
);
if (picker_color.Alpha() == 0) {
m_ts_stbitmap_custom->Show();
}
else {
m_ts_stbitmap_custom->Hide();
m_custom_cp->SetBackgroundColor(picker_color);
}
set_def_colour(picker_color);
wxCommandEvent evt(EVT_SELECTED_COLOR);
unsigned long g_col = ((picker_color.Red() & 0xff) << 24) + ((picker_color.Green() & 0xff) << 16) + ((picker_color.Blue() & 0xff) << 8) + (picker_color.Alpha() & 0xff);
evt.SetInt(g_col);
wxPostEvent(GetParent(), evt);
}
set_def_colour(*picker_color);
wxCommandEvent evt(EVT_SELECTED_COLOR);
const std::uint32_t packed = (std::uint32_t(picker_color->Red()) << 24) |
(std::uint32_t(picker_color->Green()) << 16) |
(std::uint32_t(picker_color->Blue()) << 8) | 255u;
evt.SetInt(static_cast<int>(packed));
wxPostEvent(GetParent(), evt);
}
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
-1
View File
@@ -85,7 +85,6 @@ public:
ScalableBitmap m_ts_bitmap_custom;
wxStaticBitmap* m_ts_stbitmap_custom;
StaticBox* m_custom_cp;
wxColourData* m_clrData;
StaticBox* m_def_color_box;
wxFlexGridSizer* m_ams_fg_sizer;
wxColour m_def_col;
+240
View File
@@ -0,0 +1,240 @@
#include "AnimatedTexture.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <fstream>
#include <ios>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp>
#include <boost/system/error_code.hpp>
#include <wx/animdecod.h>
#include <wx/gdicmn.h>
#include <wx/gifdecod.h>
#include <wx/image.h>
#include <wx/log.h>
#include <wx/mstream.h>
#include "slic3r/GUI/GLTexture.hpp"
namespace Slic3r::GUI {
namespace {
std::vector<unsigned char> read_file_bytes(const std::string &path)
{
std::ifstream ifs(path, std::ios::binary);
return std::vector<unsigned char>(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
bool has_gif_signature(const std::vector<unsigned char> &bytes)
{
return bytes.size() > 6 && bytes[0] == 'G' && bytes[1] == 'I' && bytes[2] == 'F';
}
// wxImage keeps colour and alpha in two separate buffers, and a file with no alpha at all has none;
// OpenGL wants the four channels interleaved.
std::vector<unsigned char> image_to_rgba(const wxImage &image)
{
const size_t n = size_t(image.GetWidth()) * size_t(image.GetHeight());
const unsigned char *rgb = image.GetData();
const unsigned char *a = image.HasAlpha() ? image.GetAlpha() : nullptr;
std::vector<unsigned char> out(n * 4);
for (size_t i = 0; i < n; ++i) {
out[i * 4 + 0] = rgb[i * 3 + 0];
out[i * 4 + 1] = rgb[i * 3 + 1];
out[i * 4 + 2] = rgb[i * 3 + 2];
out[i * 4 + 3] = a != nullptr ? a[i] : 255;
}
return out;
}
// One decoded frame on its way to the GPU: the whole animation's area, already composited.
struct DecodedFrame
{
std::vector<unsigned char> rgba;
double delay; // seconds
};
// Every frame of a GIF, composited onto the animation's own canvas.
//
// A GIF frame is a sub-rectangle that is drawn over what came before it, and the one before says how
// to clean up after itself (leave it, erase it, put back what it covered). Without replaying that, an
// animation that only re-sends the pixels that changed - which is most of them - draws as a flicker of
// disconnected patches. wxGIFDecoder hands over the raw palette indices, which is what makes replaying
// it here straightforward: a transparent index means "keep what is underneath", nothing else does.
bool decode_gif(const std::vector<unsigned char> &bytes, int &width, int &height, std::vector<DecodedFrame> &out)
{
wxMemoryInputStream stream(bytes.data(), bytes.size());
wxGIFDecoder decoder;
// A truncated file still decodes everything up to the cut, which is worth showing.
const wxGIFErrorCode err = decoder.LoadGIF(stream);
if ((err != wxGIF_OK && err != wxGIF_TRUNCATED) || decoder.GetFrameCount() == 0)
return false;
const wxSize size = decoder.GetAnimationSize();
width = size.GetWidth();
height = size.GetHeight();
if (width <= 0 || height <= 0)
return false;
std::vector<unsigned char> canvas(size_t(width) * size_t(height) * 4, 0);
std::vector<unsigned char> saved;
for (unsigned int i = 0; i < decoder.GetFrameCount(); ++i) {
const wxSize frame_size = decoder.GetFrameSize(i);
const wxPoint frame_pos = decoder.GetFramePosition(i);
const wxAnimationDisposal disposal = decoder.GetDisposalMethod(i);
const unsigned char *indices = decoder.GetData(i);
const unsigned char *palette = decoder.GetPalette(i);
const int transparent = decoder.GetTransparentColourIndex(i);
if (disposal == wxANIM_TOPREVIOUS)
saved = canvas;
if (indices != nullptr && palette != nullptr)
for (int y = 0; y < frame_size.GetHeight(); ++y) {
const int cy = frame_pos.y + y;
if (cy < 0 || cy >= height)
continue;
for (int x = 0; x < frame_size.GetWidth(); ++x) {
const int cx = frame_pos.x + x;
if (cx < 0 || cx >= width)
continue;
const unsigned char index = indices[size_t(y) * size_t(frame_size.GetWidth()) + size_t(x)];
if (int(index) == transparent)
continue;
unsigned char *dst = &canvas[(size_t(cy) * size_t(width) + size_t(cx)) * 4];
dst[0] = palette[3 * size_t(index) + 0];
dst[1] = palette[3 * size_t(index) + 1];
dst[2] = palette[3 * size_t(index) + 2];
dst[3] = 255;
}
}
// Browsers treat 0 and 1 hundredths of a second as "as fast as the file can mean", and settle
// on 100 ms; an animation authored against that convention plays at the intended speed only if
// this does the same.
const long delay_ms = decoder.GetDelay(i);
out.push_back({ canvas, (delay_ms <= 10 ? 100.0 : double(delay_ms)) / 1000.0 });
if (disposal == wxANIM_TOBACKGROUND) {
for (int y = 0; y < frame_size.GetHeight(); ++y) {
const int cy = frame_pos.y + y;
if (cy < 0 || cy >= height)
continue;
const int x0 = std::max(0, frame_pos.x);
const int x1 = std::min(width, frame_pos.x + frame_size.GetWidth());
if (x1 > x0)
std::fill_n(&canvas[(size_t(cy) * size_t(width) + size_t(x0)) * 4], size_t(x1 - x0) * 4, 0);
}
} else if (disposal == wxANIM_TOPREVIOUS) {
canvas = saved;
}
}
return !out.empty();
}
} // namespace
bool AnimatedTexture::load_from_file(const std::string &path)
{
reset();
boost::system::error_code ec;
if (!boost::filesystem::is_regular_file(path, ec))
return false;
const std::vector<unsigned char> bytes = read_file_bytes(path);
if (bytes.empty())
return false;
std::vector<DecodedFrame> frames;
int width = 0, height = 0;
if (has_gif_signature(bytes)) {
if (!decode_gif(bytes, width, height, frames)) {
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot decode GIF " << path;
return false;
}
} else {
wxLogNull no_log; // a format wxWidgets cannot read is handled here, not in a dialog
wxImage image;
wxMemoryInputStream stream(bytes.data(), bytes.size());
if (!image.LoadFile(stream, wxBITMAP_TYPE_ANY) || !image.IsOk()) {
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot decode image " << path;
return false;
}
width = image.GetWidth();
height = image.GetHeight();
frames.push_back({ image_to_rgba(image), 0.0 });
}
m_width = width;
m_height = height;
m_frames.reserve(frames.size());
for (DecodedFrame &frame : frames) {
auto texture = std::make_unique<GLTexture>();
// Mipmapped: a hint is drawn at whatever width the panel has room for, which is usually
// smaller than the source, and an unfiltered downscale of a diagram is all aliasing.
if (!texture->load_from_raw_data(std::move(frame.rgba), (unsigned int) width, (unsigned int) height, false,
/* use_mipmaps */ true)) {
BOOST_LOG_TRIVIAL(error) << "AnimatedTexture: cannot upload " << path;
reset();
return false;
}
m_frames.push_back({ std::move(texture), m_duration, frame.delay });
m_duration += frame.delay;
}
return true;
}
void AnimatedTexture::reset()
{
m_frames.clear();
m_width = 0;
m_height = 0;
m_duration = 0.;
}
unsigned int AnimatedTexture::texture_id(double time) const
{
if (m_frames.empty())
return 0;
if (m_frames.size() == 1 || m_duration <= 0.)
return m_frames.front().texture->get_id();
const double t = time - m_duration * std::floor(time / m_duration);
// Frames are few; a linear walk beats keeping an index around and getting it stale.
size_t i = 0;
while (i + 1 < m_frames.size() && t >= m_frames[i].start + m_frames[i].delay)
++i;
return m_frames[i].texture->get_id();
}
const AnimatedTexture *AnimatedTextureCache::get(const std::string &path)
{
const auto it = m_items.find(path);
if (it != m_items.end())
return it->second.get();
auto texture = std::make_unique<AnimatedTexture>();
if (!texture->load_from_file(path))
texture.reset(); // remembered as a failure, so the file is not re-read every frame
const AnimatedTexture *result = texture.get();
m_items.emplace(path, std::move(texture));
return result;
}
} // namespace Slic3r::GUI
+75
View File
@@ -0,0 +1,75 @@
#ifndef slic3r_AnimatedTexture_hpp_
#define slic3r_AnimatedTexture_hpp_
#include <map>
#include <memory>
#include <string>
#include <vector>
#include "slic3r/GUI/GLTexture.hpp"
namespace Slic3r::GUI {
// An illustration for the UI to draw: one still image, or the frames of an animated GIF together with
// the delays the file asks for. Decoded on the CPU once and uploaded as one GL texture per frame.
//
// A texture per frame rather than an atlas: these are small hints a few hundred pixels across and a
// handful of frames long, so the extra textures cost little, while drawing a frame stays a plain
// ImGui::Image with no UV arithmetic and no dependence on the maximum texture size.
//
// load_from_file() uploads to the GPU, so it needs a current GL context - call it from rendering code.
class AnimatedTexture
{
public:
AnimatedTexture() = default;
AnimatedTexture(AnimatedTexture &&) = default;
AnimatedTexture &operator=(AnimatedTexture &&) = default;
AnimatedTexture(const AnimatedTexture &) = delete;
AnimatedTexture &operator=(const AnimatedTexture &) = delete;
// PNG, JPG and BMP through wxImage; GIF, animated or not, through wxGIFDecoder. False, and nothing
// loaded, if the file is missing or cannot be decoded.
bool load_from_file(const std::string &path);
void reset();
bool is_valid() const { return !m_frames.empty(); }
bool is_animated() const { return m_frames.size() > 1; }
int width() const { return m_width; }
int height() const { return m_height; }
// Length of one loop, in seconds; 0 for a still image.
double duration() const { return m_duration; }
// The texture to draw `time` seconds into the loop, which wraps. 0 when nothing is loaded.
unsigned int texture_id(double time) const;
private:
struct Frame
{
std::unique_ptr<GLTexture> texture;
double start{0.}; // seconds into the loop this frame appears at
double delay{0.}; // seconds it stays up
};
std::vector<Frame> m_frames;
int m_width{0};
int m_height{0};
double m_duration{0.};
};
// The illustrations a panel has shown at least once, keyed by absolute path, decoded and uploaded on
// first use and kept until clear() - which has to run while the GL context is still current.
class AnimatedTextureCache
{
public:
// Null when the file is missing or cannot be decoded. The failure is remembered too, so a tooltip
// whose illustration has not been drawn yet costs one lookup, not one decode attempt per frame.
const AnimatedTexture *get(const std::string &path);
void clear() { m_items.clear(); }
private:
std::map<std::string, std::unique_ptr<AnimatedTexture>> m_items; // null value: tried once, failed
};
} // namespace Slic3r::GUI
#endif // slic3r_AnimatedTexture_hpp_
+4 -4
View File
@@ -974,12 +974,12 @@ void AuxiliaryPanel::on_import_file(wxCommandEvent &event)
if (file_model == s_default_folders[MODEL_PICTURE]) {
//wildcard = wxT("JPEG files (*.jpeg)|*.jpeg|BMP files (*.bmp)|*.bmp|GIF files (*.gif)|*.gif|PNG files (*.png)|*.png|JPG files (*.jpg)|*.jpg");
wildcard = wxT("files (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp");
wildcard = _L("Image files") + " (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";
}
if (file_model == s_default_folders[OTHERS]) { wildcard = wxT("TXT files (*.txt)|*.txt"); }
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = wxT("EXCEL files (*.xls)|*.xls|EXCEL files (*.xlsx)|*.xlsx|PDF files (*.pdf)|*.pdf"); }
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = wxT("PDF files (*.pdf)|*.pdf"); }
if (file_model == s_default_folders[OTHERS]) { wildcard = _L("TXT files") + " (*.txt)|*.txt"; }
if (file_model == s_default_folders[BILL_OF_MATERIALS]){ wildcard = _L("EXCEL files") + " (*.xls)|*.xls|" + _L("EXCEL files") + " (*.xlsx)|*.xlsx|" + _L("PDF files") + " (*.pdf)|*.pdf"; }
if (file_model == s_default_folders[ASSEMBLY_GUIDE]) { wildcard = _L("PDF files") + " (*.pdf)|*.pdf"; }
wxFileDialog dialog(this, _L("Choose files"), wxEmptyString, wxEmptyString, wildcard, wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_MULTIPLE);
if (dialog.ShowModal() == wxID_OK) {
+8 -1
View File
@@ -6,6 +6,7 @@
#include <cstddef>
#include <exception>
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Config.hpp"
#include <cstdint>
#include <functional>
@@ -57,6 +58,7 @@
#include "libslic3r_version.h"
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/GCode/GCodeStatistics.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/Utils/PrintHost.hpp"
@@ -122,7 +124,7 @@ std::pair<std::string, std::vector<size_t>> SlicingProcessCompletedEvent::format
} catch (std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown C++ exception.";
error = _u8L("Unknown C++ exception.");
}
return std::make_pair(std::move(error), std::vector<size_t>{monospace});
}
@@ -781,6 +783,7 @@ void BackgroundSlicingProcess::schedule_export(const std::string& path, bool exp
this->invalidate_step(bspsGCodeFinalize);
m_export_path = path;
m_export_path_on_removable_media = export_path_on_removable_media;
m_export_statistics = GUI::wxGetApp().app_config->get_bool("export_statistics");
}
void BackgroundSlicingProcess::schedule_upload(Slic3r::PrintHostJob upload_job)
@@ -906,6 +909,8 @@ void BackgroundSlicingProcess::finalize_gcode()
BOOST_LOG_TRIVIAL(error) << "Unexpected fail code(" << (int) copy_ret_val << ") durring copy_file() to " << export_path << ".";
break;
}
if (m_export_statistics)
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
m_print->set_status(100, GUI::format(_L("G-code file exported to %1%"), export_path));
}
@@ -953,6 +958,8 @@ void BackgroundSlicingProcess::export_gcode()
// throw Slic3r::ExportError(_utf8(L("Unknown error when exporting G-code.")));
break;
}
if (m_export_statistics)
export_gcode_statistics(*m_gcode_result, gcode_statistics_path(export_path));
// BBS
auto evt = new wxCommandEvent(m_event_export_finished_id, GUI::wxGetApp().mainframe->m_plater->GetId());
@@ -258,6 +258,8 @@ private:
// but once set, it cannot be re-set.
std::string m_export_path;
bool m_export_path_on_removable_media = false;
// Read from the preferences when the export is scheduled, as the background thread may not read them.
bool m_export_statistics = false;
// Print host upload job to schedule after slicing is complete, used by schedule_upload(),
// empty by default (ie. no upload to schedule)
PrintHostJob m_upload_job;
+67 -50
View File
@@ -931,7 +931,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
});
feat_dropdown("plane", "design_plane", _L("Datum / Curve (plane / axis / coord sys / helix / project)"), {
{"design_plane", _L("Plane"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
{"design_plane", _L_CONTEXT("Plane", "Design"), _L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"),
[this] {
populate_plane_choices(m_plane_base);
reset_plane_refs();
@@ -1011,7 +1011,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
open_tool(Tool::Transform);
}, SHIFT('Y')},
{"design_mirror", _L("Mirror"), _L("Reflect a body about a plane"),
{"design_mirror", _L_CONTEXT("Mirror", "Design"), _L("Reflect a body about a plane"),
[this] {
if (m_doc.bodies.empty()) {
set_status(StatusKind::Error, _L("Mirror needs a body — add or import one first"));
@@ -1143,7 +1143,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// Hole / Thread — drilling into a solid (both face-aware)
feat_dropdown("hole", "design_hole", _L("Hole / thread"), {
{"design_hole", _L("Hole"), _L("Drill a hole, centered on a picked face or placed on a plane"),
{"design_hole", _L_CONTEXT("Hole", "Design"), _L("Drill a hole, centered on a picked face or placed on a plane"),
[this] {
// #2: drill on the picked solid face, centred on it (origin = face centroid,
// normal = inward). Otherwise fall back to the plane dropdown. m_hole_x/y then
@@ -1344,7 +1344,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
_L("Click 2 points or a line / circle / arc to place a dimension"));
// (separator dropped: the group it divided is now reached from the offer)
dropdown("design_line", _L("Line / polyline"), {
{"design_line", DesignSketchTool::Mode::Line, _L("Line"), _L("Click start, then end — then set the exact length")},
{"design_line", DesignSketchTool::Mode::Line, _L_CONTEXT("Line", "Design"), _L("Click start, then end — then set the exact length")},
{"design_polyline", DesignSketchTool::Mode::Polyline, _L("Polyline"), _L("Click points; click the first point to close the loop, right-click or Enter to end the chain")} });
dropdown("design_rect", _L("Rectangle"), {
{"design_rect", DesignSketchTool::Mode::CornerRect, _L("Corner rectangle"), _L("Click two opposite corners")},
@@ -1407,7 +1407,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
{"design_chamfer", DesignSketchTool::Mode::Chamfer, _L("Chamfer"), _L("Pick two lines, then drag the arrow or click the distance to set it")} });
skbtn("design_offset", DesignSketchTool::Mode::Offset, _L("Offset"),
_L("Pick an entity, then drag the arrow or click the distance; click empty to apply"));
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L("Mirror"),
skbtn("design_mirror", DesignSketchTool::Mode::Mirror, _L_CONTEXT("Mirror", "Design"),
_L("Pick a mirror-axis line, then the entities to mirror; click empty to apply"));
// Trim / Extend scissors — standalone sketch tools (NOT inside Constrain): click a
// segment to cut it back to / out to its nearest intersection. One cut per click.
@@ -1438,9 +1438,12 @@ DesignPanel::DesignPanel(wxWindow* parent)
auto arm_polygon = [this, select_tool] {
push_polygon_params();
select_tool(DesignSketchTool::Mode::Polygon,
wxString::Format(_L("Click center then a vertex — %d sides, %s"),
m_poly_sides,
m_poly_circumscribed ? _L("circumscribed") : _L("inscribed")));
wxString::Format(m_poly_circumscribed
? _L_PLURAL("Click center then a vertex — %d side, circumscribed",
"Click center then a vertex — %d sides, circumscribed", m_poly_sides)
: _L_PLURAL("Click center then a vertex — %d side, inscribed",
"Click center then a vertex — %d sides, inscribed", m_poly_sides),
m_poly_sides));
};
for (int n : {3, 4, 5, 6, 8, 12})
m_verb_actions["btn:poly#" + std::to_string(n)] =
@@ -1998,7 +2001,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
hform->Add(m_hole_target_label, 0, wxALIGN_CENTER_VERTICAL);
m_box_hole = new wxBoxSizer(wxVERTICAL);
m_box_hole->Add(card_header(m_cards, "design_hole", _L("Hole"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_hole->Add(card_header(m_cards, "design_hole", _L_CONTEXT("Hole", "Design"), m_hdr_hole), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_hole->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
m_box_hole->Add(hform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12);
cards->Add(m_box_hole, 0, wxEXPAND);
@@ -2022,7 +2025,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_thread_std->Append(s.name);
m_thread_std->SetSelection(0);
m_thread_std->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { apply_thread_standard(); });
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Standard")), 0, wxALIGN_CENTER_VERTICAL);
tform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Standard", "Thread standard")), 0, wxALIGN_CENTER_VERTICAL);
tform->Add(m_thread_std, 0, wxEXPAND);
// Threads are specified by DIAMETER (M6 = Ø6); the value is derived from the picked cylindrical
@@ -2233,7 +2236,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_cut_plane = make_combo(m_cards);
m_cut_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
cform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
cform->Add(m_cut_plane, 0, wxEXPAND);
m_cut_offset = make_spin(m_cards, 0.0, -10000.0, 10000.0);
@@ -2258,7 +2261,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// --- Plane (datum/reference plane: offset + tilt from a base plane; no solid) ---
m_box_plane = new wxBoxSizer(wxVERTICAL);
m_box_plane->Add(card_header(m_cards, "design_plane", _L("Plane"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_plane->Add(card_header(m_cards, "design_plane", _L_CONTEXT("Plane", "Design"), m_hdr_plane), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_plane->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
{
// Plane type chooses which inputs matter (Onshape/Fusion parity):
@@ -2570,7 +2573,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// --- Mirror (reflect a body about a plane) ---
m_box_mirror = new wxBoxSizer(wxVERTICAL);
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L("Mirror"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_mirror->Add(card_header(m_cards, "design_mirror", _L_CONTEXT("Mirror", "Design"), m_hdr_mirror), 0, wxLEFT | wxRIGHT | wxTOP, 12);
m_box_mirror->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8);
{
auto* mform = two_col_form();
@@ -2748,7 +2751,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_helix_plane = make_combo(m_cards);
populate_plane_choices(m_helix_plane);
m_helix_plane->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); });
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Plane")), 0, wxALIGN_CENTER_VERTICAL);
hform->Add(new wxStaticText(m_cards, wxID_ANY, _L_CONTEXT("Plane", "Design")), 0, wxALIGN_CENTER_VERTICAL);
hform->Add(m_helix_plane, 0, wxEXPAND);
m_helix_radius = make_spin(m_cards, 10.0, 0.01, 10000.0);
m_helix_radius->Bind(wxEVT_SPINCTRLDOUBLE, [this](wxSpinDoubleEvent&) { refresh_preview(); });
@@ -3556,7 +3559,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_doc.recompute();
set_status(StatusKind::Info, cons.empty()
? _L("Sketch created — select it, then right-click to Extrude")
: wxString::Format(_L("Sketch created (%zu driving dims) — select it, then right-click to Extrude"),
: wxString::Format(_L_PLURAL("Sketch created (%zu driving dim) — select it, then right-click to Extrude",
"Sketch created (%zu driving dims) — select it, then right-click to Extrude",
unsigned(cons.size())),
cons.size()));
refresh_tree();
sync_sketch_display(); // keep the just-committed sketch visible as a face
@@ -4585,8 +4590,8 @@ void DesignPanel::on_shape_changed()
void DesignPanel::set_status_ok()
{
set_status(wxString::Format(_L("OK — %zu triangles"),
m_doc.display_mesh.its.indices.size()));
const size_t n_tris = m_doc.display_mesh.its.indices.size();
set_status(wxString::Format(_L_PLURAL("OK — %zu triangle", "OK — %zu triangles", unsigned(n_tris)), n_tris));
if (m_viewport != nullptr) {
// A recompute invalidates the gizmo's body centroid; a Move ends with it, keeping its pose.
if (body_move_pending()) end_body_move(true);
@@ -5035,11 +5040,15 @@ void DesignPanel::on_import_mesh()
// A dense organic scan has few coplanar neighbours to merge away and stays heavy afterwards;
// warn rather than silently freezing the CAD kernel on every subsequent recompute.
if (mesh.its.indices.size() > MESH_IMPORT_TRIANGLE_WARN) {
const int n_tris = int(mesh.its.indices.size());
const wxString q = wxString::Format(
_L("This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
"merging, so importing it may take a long time and leave a body that is slow to "
"edit. Decimating the mesh first is usually better.\n\nImport anyway?"),
int(mesh.its.indices.size()));
_L_PLURAL("This mesh has %d triangle. Every triangle becomes a B-rep face before coplanar "
"merging, so importing it may take a long time and leave a body that is slow to "
"edit. Decimating the mesh first is usually better.\n\nImport anyway?",
"This mesh has %d triangles. Every triangle becomes a B-rep face before coplanar "
"merging, so importing it may take a long time and leave a body that is slow to "
"edit. Decimating the mesh first is usually better.\n\nImport anyway?", n_tris),
n_tris);
MessageDialog dlg(this, q, _L("Large mesh"), wxYES_NO | wxICON_WARNING);
if (dlg.ShowModal() != wxID_YES)
return;
@@ -5080,7 +5089,7 @@ void DesignPanel::on_import_mesh()
// source mesh the user needs to know about before they start cutting features into it.
if (stats.is_solid) {
set_status(StatusKind::Info, wxString::Format(
_L("Imported solid — %d triangles → %d faces, volume %.2f mm³. Pick a face or edge, "
_L("Imported solid — triangles: %d → faces: %d, volume %.2f mm³. Pick a face or edge, "
"then Fillet / Cut / Shell to modify"),
stats.kept_tris, stats.faces_final, stats.volume));
} else {
@@ -5171,7 +5180,8 @@ void DesignPanel::open_insert_card(const wxString& base_name)
update_cards_frame(); m_form->Layout();
m_form->FitInside();
update_action_bar(); // surface the unified ✓/✗
set_status(StatusKind::Info, base_name + _L(" — drag to place/size, then Confirm"));
// TRN Design tab status; %s is the inserted item, such as "Text" or "SVG".
set_status(StatusKind::Info, wxString::Format(_L("%s — drag to place/size, then Confirm"), base_name));
}
// Confirm: keep the placed art and leave the placement gizmo. The feature is already in
@@ -5255,7 +5265,7 @@ void DesignPanel::on_add_extrude()
{
BooleanMode mode = static_cast<BooleanMode>(m_mode->GetSelection()); // New/Add/Cut/Intersect
m_feature_counter++;
const std::string name = feature_name(_L("Extrude"));
const std::string name = feature_name(_L_CONTEXT("Extrude", "Design"));
int idx = -1;
if (m_extrude_face_src >= 0) {
// Onshape face-extrude: the picked solid face is the profile (no sketch wire).
@@ -5383,7 +5393,7 @@ void DesignPanel::on_add_hole()
m_feature_counter++;
const int hidx = m_doc.add_hole(dia, depth, through, px, py, plane,
feature_name(_L("Hole")));
feature_name(_L_CONTEXT("Hole", "Design")));
// On-face holes drill the body the face belongs to (even after the pick was cleared).
if (m_hole_on_face && hidx >= 0 && hidx < int(m_doc.features.size()))
m_doc.features[hidx].target_body = m_hole_face_body;
@@ -5770,7 +5780,7 @@ void DesignPanel::on_add_mirror()
const BooleanMode mode = m_mirror_keep->GetValue() ? BooleanMode::New : BooleanMode::Add;
m_feature_counter++;
m_doc.add_mirror(plane_from_choice(m_mirror_plane->GetSelection()), target, mode,
feature_name(_L("Mirror")));
feature_name(_L_CONTEXT("Mirror", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -5817,7 +5827,7 @@ void DesignPanel::on_add_rib()
}
m_feature_counter++;
m_doc.add_rib(sketch_ref, m_rib_entity->GetValue(), m_rib_thickness->GetValue(),
m_rib_depth->GetValue(), target, feature_name(_L("Rib")));
m_rib_depth->GetValue(), target, feature_name(_L_CONTEXT("Rib", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -5837,7 +5847,7 @@ void DesignPanel::on_add_project()
m_feature_counter++;
m_doc.add_project_edges(src_body, {}, face,
plane_from_choice(m_proj_plane->GetSelection()),
feature_name(_L("Project")));
feature_name(_L_CONTEXT("Project", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -6024,7 +6034,7 @@ void DesignPanel::on_add_pattern()
m_doc.add_pattern(circular, int(m_pattern_count->GetValue()),
m_pattern_spacing->GetValue(), m_pattern_dir->GetSelection(),
m_pattern_angle->GetValue(), target,
feature_name(_L("Pattern")));
feature_name(_L_CONTEXT("Pattern", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
@@ -6126,7 +6136,7 @@ void DesignPanel::on_add_cut()
m_feature_counter++;
m_doc.add_cut(plane_from_choice(m_cut_plane->GetSelection()), m_cut_offset->GetValue(),
/*flip*/ false, /*keep_upper*/ true, /*keep_lower*/ true,
m_cut_target->GetSelection(), feature_name(_L("Cut")));
m_cut_target->GetSelection(), feature_name(_L_CONTEXT("Cut", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
else
@@ -6429,7 +6439,8 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
using Mode = DesignSketchTool::Mode;
auto pick_more = [](const wxString& lead, int n) {
return n <= 0 ? lead
: lead + wxString::Format(_L(" · %d picked"), n);
// TRN Appended to a sketch tool prompt; %d is the number of picked entities.
: lead + wxString::Format(_L_PLURAL(" · %d picked", " · %d picked", n), n);
};
switch (m) {
case Mode::Select:
@@ -6442,8 +6453,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
"Shift-click adds · Del removes");
case Mode::Constrain:
return picks > 0
? wxString::Format(_L("%d picked · now choose a constraint (Horizontal, Parallel, "
"Tangent, Equal…)"), picks)
? wxString::Format(_L_PLURAL("%d picked · now choose a constraint (Horizontal, Parallel, "
"Tangent, Equal…)",
"%d picked · now choose a constraint (Horizontal, Parallel, "
"Tangent, Equal…)", picks), picks)
: _L("Constrain — click one or two entities, then choose a constraint");
case Mode::Dimension:
return step == 0 ? _L("Dimension — click an entity, or the first of two points")
@@ -6544,8 +6557,10 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks
? _L("Mirror — first click the LINE to mirror about (a construction line works)")
: picks == 0
? _L("Mirror — axis set · now click the entities to mirror · Esc cancels")
: wxString::Format(_L("Mirror — axis set · %d to mirror · click another to add or "
"remove it · Enter applies"), picks);
: wxString::Format(_L_PLURAL("Mirror — axis set · %d to mirror · click another to add or "
"remove it · Enter applies",
"Mirror — axis set · %d to mirror · click another to add or "
"remove it · Enter applies", picks), picks);
case Mode::Move:
return step == 0 ? _L("Move — click the entities to move")
: pick_more(_L("Move — drag the handle, or click the number to type the "
@@ -6653,7 +6668,7 @@ wxString DesignPanel::offer_header(int kind) const
case OfferSel::SkPoint: return _L("Sketch point");
case OfferSel::Sk2Ent: {
const int n = m_viewport ? m_viewport->sketch_selection_count() : 0;
return wxString::Format(_L("%d sketch entities"), n);
return wxString::Format(_L_PLURAL("%d sketch entity", "%d sketch entities", n), n);
}
default: return wxString();
}
@@ -6753,7 +6768,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
} else {
offer_trace("row=%d %s DISABLED (%s)", row, kOfferRowNames[row],
v->refusal ? v->refusal : "blocked");
wxString s = tr(v->name);
wxString s = I18N::translate(v->name, "Design");
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
append_offer_item(&menu, id, s, *v)->Enable(false);
bound.push_back(nullptr);
@@ -6828,7 +6843,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
}
if (!ok) {
offer_trace("row=%d %s ~ %s BLOCKED", row, kOfferRowNames[row], v->id);
wxString s = tr(v->name);
wxString s = I18N::translate(v->name, "Design");
if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal);
append_offer_item(target, base + int(bound.size()), s, *v)->Enable(false);
bound.push_back(nullptr);
@@ -6935,8 +6950,8 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos)
if (i < 0 || i >= int(opts.size()) || !opts[i].viable) return;
drop_ghost(); // the real bodies are about to become the ghost's pose
m_doc.checkpoint(); // undo boundary: committing a mate from the offer
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false,
"Mate" + std::to_string(++m_feature_counter));
++m_feature_counter;
int idx = m_doc.add_mate(opts[i].kind, cs_a, cs_b, 0.0, 0.0, false, feature_name(_L("Mate")));
if (idx < 0) {
set_status(StatusKind::Error, _L("Mate rejected"));
return;
@@ -7165,7 +7180,7 @@ bool DesignPanel::on_add_plane()
m_feature_counter++;
int idx = m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(),
m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(),
feature_name(_L("Plane")));
feature_name(_L_CONTEXT("Plane", "Design")));
if (idx >= 0 && idx < int(m_doc.features.size())) apply_plane_refs(m_doc.features[idx]);
m_doc.recompute(); // datum-only docs yield no body; that is expected/benign
set_status(StatusKind::Info, _L("Plane added — pick it as a sketch plane"));
@@ -7206,7 +7221,7 @@ void DesignPanel::on_add_shell()
m_feature_counter++;
m_doc.add_shell(m_shell_thickness->GetValue(), face, m_sel_solid_body,
feature_name(_L("Shell")));
feature_name(_L_CONTEXT("Shell", "Design")));
if (!recompute_guarded(_L("Rebuilding model…")))
set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error)));
@@ -9500,7 +9515,8 @@ void DesignPanel::request_value(const wxString& label, double def, double mn, do
m_form->FitInside();
m_value_input->SetFocus();
m_value_input->SetSelection(-1, -1); // select all so typing replaces the value
set_status(StatusKind::Info, label + _L(" — type a value, press Enter (or Confirm)"));
// TRN Design tab status; %s is the value being asked for, such as "Fillet radius".
set_status(StatusKind::Info, wxString::Format(_L("%s — type a value, press Enter (or Confirm)"), label));
}
void DesignPanel::confirm_value()
@@ -10228,19 +10244,19 @@ void DesignPanel::on_commit()
for (size_t b = 0; b < m_disp_body_meshes.size(); ++b) {
if (b < m_body_visible.size() && !m_body_visible[b]) continue; // skip hidden
if (m_disp_body_meshes[b].its.indices.empty()) continue;
parts.emplace_back(&m_disp_body_meshes[b], wxString::FromUTF8("Design Body " + std::to_string(b + 1)));
parts.emplace_back(&m_disp_body_meshes[b], wxString::Format(_L("Design Body %d"), int(b + 1)));
}
if (parts.empty()) { // every body hidden — nothing to ship
set_status(_L("All bodies hidden — show one before committing"));
return;
}
if (m_commit_mode == CommitMode::Assembly && parts.size() > 1)
obj_list->load_mesh_object(parts, "Design Assembly");
obj_list->load_mesh_object(parts, _L("Design Assembly"));
else
for (const auto& [mesh, name] : parts)
obj_list->load_mesh_object(*mesh, name);
} else {
obj_list->load_mesh_object(m_disp_pick_mesh, "Design Body");
obj_list->load_mesh_object(m_disp_pick_mesh, _L("Design Body"));
}
// Persist the editable parametric recipe alongside the committed meshes so the
@@ -11478,7 +11494,8 @@ void DesignPanel::refresh_preview()
if (ok) {
m_viewport->set_preview_mesh(mesh);
set_status(StatusKind::Ok, wxString::Format(_L("Preview — %zu triangles"), mesh.its.indices.size()));
const size_t n_tris = mesh.its.indices.size();
set_status(StatusKind::Ok, wxString::Format(_L_PLURAL("Preview — %zu triangle", "Preview — %zu triangles", unsigned(n_tris)), n_tris));
} else {
m_viewport->clear_preview();
set_status(StatusKind::Error, wxString::Format(_L("Invalid: %s"), kernel_error_text(err)));
@@ -11789,13 +11806,13 @@ void DesignPanel::open_tool(Tool t)
case Tool::Extrude: m_hdr_extrude->SetLabel(title(_L_CONTEXT("Extrude", "Design"))); break;
case Tool::Dressup: m_hdr_dressup->SetLabel(title(
m_dressup_type->GetSelection() == 0 ? _L("Fillet") : _L("Chamfer"))); break;
case Tool::Hole: m_hdr_hole->SetLabel(title(_L("Hole"))); break;
case Tool::Hole: m_hdr_hole->SetLabel(title(_L_CONTEXT("Hole", "Design"))); break;
case Tool::Thread: m_hdr_thread->SetLabel(title(_L("Thread"))); break;
case Tool::Shell: m_hdr_shell->SetLabel(title(_L_CONTEXT("Shell", "Design"))); break;
case Tool::Revolve: m_hdr_revolve->SetLabel(title(_L("Revolve"))); break;
case Tool::Sweep: m_hdr_sweep->SetLabel(title(_L("Sweep"))); break;
case Tool::Pattern: m_hdr_pattern->SetLabel(title(_L_CONTEXT("Pattern", "Design"))); break;
case Tool::Plane: m_hdr_plane->SetLabel(title(_L("Plane"))); break;
case Tool::Plane: m_hdr_plane->SetLabel(title(_L_CONTEXT("Plane", "Design"))); break;
case Tool::Loft: m_hdr_loft->SetLabel(title(_L("Loft"))); break;
case Tool::Draft: m_hdr_draft->SetLabel(title(_L("Draft"))); break;
case Tool::Boolean: m_hdr_boolean->SetLabel(title(_L("Boolean"))); break;
@@ -11809,7 +11826,7 @@ void DesignPanel::open_tool(Tool t)
case Tool::SurfaceOffset: m_hdr_surf_offset->SetLabel(title(_L("Surface Offset"))); break;
case Tool::ThickenSurface: m_hdr_surf_thicken->SetLabel(title(_L("Thicken Surface"))); break;
case Tool::Transform: m_hdr_transform->SetLabel(title(_L("Transform"))); break;
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L("Mirror"))); break;
case Tool::Mirror: m_hdr_mirror->SetLabel(title(_L_CONTEXT("Mirror", "Design"))); break;
case Tool::Thicken: m_hdr_thicken->SetLabel(title(_L("Thicken"))); break;
case Tool::Rib: m_hdr_rib->SetLabel(title(_L_CONTEXT("Rib", "Design"))); break;
case Tool::Project: m_hdr_project->SetLabel(title(_L_CONTEXT("Project", "Design"))); break;
+10 -4
View File
@@ -5,6 +5,7 @@
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/Utils.hpp"
@@ -902,8 +903,11 @@ void DesignSketchTool::resolve_live_drag(int dragged_ei, SketchPointRole dragged
if (int(r.skipped.size()) != m_skipped_last) {
m_skipped_last = int(r.skipped.size());
if (m_skipped_last > 0)
notify(format(_u8L("%1% constraint(s) cannot act on this geometry (an ellipse or spline curve, "
"or a zero-size circle) and are ignored"), m_skipped_last), false);
notify(format(into_u8(_L_PLURAL("%1% constraint cannot act on this geometry (an ellipse or spline curve, "
"or a zero-size circle) and is ignored",
"%1% constraints cannot act on this geometry (an ellipse or spline curve, "
"or a zero-size circle) and are ignored", m_skipped_last)),
m_skipped_last), false);
}
// Flag every entity referenced by a conflicting constraint so render() can
// tint it red (Onshape/SolveSpace over-constrained feedback), and remember which
@@ -8978,7 +8982,8 @@ void DesignSketchTool::confirm_op()
|| self_len(d, a) || self_len(d, b);
});
if (released > 0)
notify(format(_u8L("%1% constraint(s) the fillet/chamfer invalidated were removed"), released), false);
notify(format(into_u8(_L_PLURAL("%1% constraint the fillet/chamfer invalidated was removed",
"%1% constraints the fillet/chamfer invalidated were removed", released)), released), false);
auto coin = [&](R xr, int ln, R lr) {
SketchEntityConstraintDef d; d.type = CT::Coincident; d.ea = xi; d.ra = xr; d.eb = ln; d.rb = lr; return d; };
if (fillet) {
@@ -9427,7 +9432,8 @@ void DesignSketchTool::confirm_transform()
}
});
if (released > 0)
notify(format(_u8L("%1% constraint(s) released by the transform"), released), false);
notify(format(into_u8(_L_PLURAL("%1% constraint released by the transform",
"%1% constraints released by the transform", released)), released), false);
} else if (m_mode == Mode::Array || m_mode == Mode::PolarArray) {
// ADDITIVE: append copies of each subject, then bind each copy to its source. Lines
// get Parallel+EqualLength (linear) or EqualLength only (polar — rotation breaks
+1 -1
View File
@@ -559,7 +559,7 @@ int HistoryWindow::get_extruder_id()
void HistoryWindow::on_click_new_button(wxCommandEvent& event)
{
if (curr_obj && curr_obj->get_printer_series() == PrinterSeries::SERIES_P1P && m_calib_results_history.size() >= 16) {
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle."), 16), wxEmptyString, wxICON_WARNING | wxOK);
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle.", "This machine type can only hold %d historical results per nozzle.", 16), 16), wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
return;
}
+1 -1
View File
@@ -973,7 +973,7 @@ bool PressureAdvanceWizard::can_save_cali_result(const std::vector<PACalibResult
}
if (curr_obj->get_printer_series() != PrinterSeries::SERIES_X1 && curr_obj->pa_calib_tab.size() >= MAX_PA_HISTORY_RESULTS_NUMS) {
MessageDialog msg_dlg(nullptr, wxString::Format(_L("This machine type can only hold %d historical results per nozzle. This result will not be saved."), MAX_PA_HISTORY_RESULTS_NUMS),
MessageDialog msg_dlg(nullptr, wxString::Format(_L_PLURAL("This machine type can only hold %d historical result per nozzle. This result will not be saved.", "This machine type can only hold %d historical results per nozzle. This result will not be saved.", MAX_PA_HISTORY_RESULTS_NUMS), MAX_PA_HISTORY_RESULTS_NUMS),
wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
return false;
+3 -1
View File
@@ -352,7 +352,9 @@ void CalibrationCaliPage::update_subtask(MachineObject* obj)
prepare_text = wxString::Format(_L("Cloud Slicing..."));
}
else {
prepare_text = wxString::Format(_L("In Cloud Slicing Queue, there are %s tasks ahead of you."), std::to_string(obj->queue_number));
prepare_text = wxString::Format(_L_PLURAL("In Cloud Slicing Queue, there is %d task ahead of you.",
"In Cloud Slicing Queue, there are %d tasks ahead of you.", obj->queue_number),
obj->queue_number);
show_percent = false;
}
}
+1 -1
View File
@@ -249,7 +249,7 @@ CaliPageButton::CaliPageButton(wxWindow* parent, CaliPageActionType type, wxStri
this->SetLabel(_L("Finish"));
break;
default:
this->SetLabel("Unknown");
this->SetLabel(_L("Unknown"));
break;
}
@@ -644,9 +644,9 @@ void CalibrationPresetPage::create_selection_panel(wxWindow* parent)
for (const auto& extruder : curr_obj->GetExtderSystem()->GetExtruders()) {
if (extruder.GetNozzleType() == NozzleType::ntUndefine) {
wxString name = _L("left");
if (extruder.GetExtId() == 0) { name = _L("right"); }
wxString msg = wxString::Format(_L("Printer %s nozzle information has not been set. Please configure it before proceeding with the calibration."), name);
wxString msg = extruder.GetExtId() == 0 ?
_L("Printer right nozzle information has not been set. Please configure it before proceeding with the calibration.") :
_L("Printer left nozzle information has not been set. Please configure it before proceeding with the calibration.");
MessageDialog msg_dlg(nullptr, msg, wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
break;
@@ -2815,7 +2815,7 @@ MaxVolumetricSpeedPresetPage::MaxVolumetricSpeedPresetPage(
wxArrayString titles;
titles.push_back(_L("From Volumetric Speed"));
titles.push_back(_L("To Volumetric Speed"));
titles.push_back(_L("Step"));
titles.push_back(_L_CONTEXT("Step", "Increment"));
m_custom_range_panel->set_titles(titles);
m_custom_range_panel->set_unit(_L("mm³/s"));
+9 -10
View File
@@ -680,16 +680,15 @@ void ColorDecomposeDialog::update_filament_limit_warning()
return;
}
wxString mode_name;
switch (m_selected_mode) {
case DecomposeMode::CMYW: mode_name = "CMYW"; break;
case DecomposeMode::RYBW: mode_name = "RYBW"; break;
case DecomposeMode::MaterialList: mode_name = _L("Material List"); break;
}
const wxString warning_text = format_wxstr(
_L("The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing."),
m_max_filament_count, mode_name);
const unsigned int max_count = static_cast<unsigned int>(m_max_filament_count);
const wxString warning_text = m_selected_mode == DecomposeMode::MaterialList ?
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
"The material list supports at most %1% colors. After Material List decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
max_count), max_count) :
// TRN %2% is the decomposition mode name, "CMYW" or "RYBW"
format_wxstr(_L_PLURAL("The material list supports at most %1% color. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
"The material list supports at most %1% colors. After %2% decomposition, the material count would exceed %1%. Please delete unused filaments on the main screen before decomposing.",
max_count), max_count, m_selected_mode == DecomposeMode::CMYW ? "CMYW" : "RYBW");
// Show first so the panel is laid out and the text control gets its real
// width, then wrap to that width so the paragraph fills the content area.
+9 -4
View File
@@ -4,6 +4,7 @@
#include "GUI_App.hpp"
#include "MsgDialog.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Utils.hpp"
@@ -340,8 +341,9 @@ bool prepare_decompose_mixed_result(
missing_comp.component_idx = out_result.components.size();
missing_comp.official_component = official_component;
missing_comp.preset_name = preset_name;
missing_comp.display_name = decompose_base_color_display(comp.base_color) +
wxString::FromUTF8(" ") + wxString::FromUTF8(basic_type);
// TRN %1% is a color name such as "Cyan", %2% is a filament type such as "PLA Basic"
missing_comp.display_name = format_wxstr(_L_CONTEXT("%1% %2%", "Color and filament type"),
decompose_base_color_display(comp.base_color), wxString::FromUTF8(basic_type));
missing.push_back(std::move(missing_comp));
out_result.components.push_back(0);
}
@@ -407,8 +409,11 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
missing_text += missing[i].display_name;
}
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
"the mixed filament will be created automatically after decomposition."),
wxString message = wxString::Format(_L_PLURAL("The current filament list does not contain %s. A project filament required by "
"the mixed filament will be created automatically after decomposition.",
"The current filament list does not contain %s. Project filaments required by "
"the mixed filament will be created automatically after decomposition.",
missing.size()),
missing_text);
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
File diff suppressed because it is too large. Load diff
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include "libslic3r/Color.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include <array>
#include <cstddef>
#include <variant>
#include <nlohmann/json_fwd.hpp>
#include <atomic>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <wx/gdicmn.h>
class wxWindow;
namespace Slic3r::GUI {
// Most colors an ordered gradient may hold. This is the native switch for multi-color gradients:
// 2 keeps the classic two-color gradients. The page receives it as options.max_gradient_colors
// and never offers more, so the picker, favorites and project colors all share one limit.
inline constexpr std::size_t COLOR_PICKER_MAX_GRADIENT_COLORS = 7;
// Gradient colors retain their order (2..COLOR_PICKER_MAX_GRADIENT_COLORS); a solid has exactly one color.
using ColorGradient = std::vector<ColorRGBA>;
using ColorSelection = std::variant<ColorRGBA, ColorGradient>;
struct ColorPickerOptions {
bool allow_gradient = false;
bool allow_alpha = false;
};
// Project storage preserves direction; FilamentColor's set is only for the
// official palette and cannot represent ordered custom gradients.
struct FilamentColorPickerValue {
std::vector<std::string> colors;
bool gradient = false;
std::string primary;
};
std::optional<ColorSelection> filament_color_picker_selection(const FilamentColorPickerValue& value);
ColorSelection filament_color_picker_initial(const FilamentColorPickerValue& value);
FilamentColorPickerValue filament_color_picker_result(const ColorSelection& selection);
bool filament_color_picker_changed(const ColorSelection& initial, const std::optional<ColorSelection>& result);
class ColorPickerDialog : public WebViewHostDialog
{
public:
ColorPickerDialog(wxWindow* parent, const ColorSelection& initial, ColorPickerOptions options = {},
bool preserve_multi_color = false, wxWindow* anchor = nullptr);
~ColorPickerDialog() override;
const std::optional<ColorSelection>& selection() const { return m_selection; }
private:
void add_user_scripts() override;
void on_script_message(const nlohmann::json& payload) override;
void handle_web_command(const nlohmann::json& payload);
void send_initial_state();
void finish(int return_code);
void position_panel();
void move_to_anchor();
void resize_to_content(int height);
void focus_webview();
void repaint_webview();
void on_dpi_changed(const wxRect& suggested_rect) override;
void on_sys_color_changed() override;
ColorSelection m_initial;
ColorPickerOptions m_options;
std::optional<ColorSelection> m_selection;
std::vector<ColorSelection> m_favorites;
std::string m_page_id;
bool m_preserve_multi_color = false;
bool m_init_sent = false;
bool m_page_ready = false;
bool m_closing = false;
bool m_favorites_writable = false;
bool m_sidebar_open = false;
std::string m_palette;
bool m_initial_positioned = false;
wxRect m_anchor_rect;
wxRect m_work_area;
int m_content_height = 360;
int m_content_width = 330;
bool m_positioning = false;
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
};
} // namespace Slic3r::GUI
+2 -1
View File
@@ -1,5 +1,6 @@
#include "ConnectPrinter.hpp"
#include "GUI_App.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
@@ -169,7 +170,7 @@ void ConnectPrinterDialog::on_input_enter(wxCommandEvent& evt)
void ConnectPrinterDialog::on_button_confirm(wxCommandEvent &event)
{
wxString code = m_textCtrl_code->GetTextCtrl()->GetValue();
if (code.empty())
if (code.empty() && (!m_obj || m_obj->printer_agent_id != MOONRAKER_PRINTER_AGENT_ID))
code = "88888888";
for (char c : code) {
if (!(('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z'))) {
+3 -2
View File
@@ -35,7 +35,7 @@ CrealityDiscoveryDialog::CrealityDiscoveryDialog(wxWindow* parent)
m_list = new wxListView(this, wxID_ANY, wxDefaultPosition, wxDefaultSize,
wxLC_REPORT | wxSIMPLE_BORDER | wxLC_SINGLE_SEL);
m_list->SetMinSize(wxSize(50 * em, 18 * em));
m_list->AppendColumn(_L("Model"), wxLIST_FORMAT_LEFT, 8 * em);
m_list->AppendColumn(_L_CONTEXT("Model", "Printer model"), wxLIST_FORMAT_LEFT, 8 * em);
m_list->AppendColumn(_L("Hostname"), wxLIST_FORMAT_LEFT, 14 * em);
m_list->AppendColumn(_L("IP"), wxLIST_FORMAT_LEFT, 14 * em);
@@ -109,7 +109,8 @@ void CrealityDiscoveryDialog::run_discovery()
"network and not blocked by Wi-Fi client isolation, then click Scan again."));
} else {
m_status->SetLabel(wxString::Format(
_L("Found %zu Creality printer(s). Select one and click Use Selected."),
_L_PLURAL("Found %zu Creality printer. Select it and click Use Selected.",
"Found %zu Creality printers. Select one and click Use Selected.", m_rows.size()),
m_rows.size()));
m_list->Select(0);
}
+1 -1
View File
@@ -4690,7 +4690,7 @@ void EditFilamentPresetDialog::delete_preset()
if (count > 0) {
msg = _L("Presets inherited by other presets cannot be deleted");
msg += "\n";
msg += _L_PLURAL("The following presets inherits this preset.", "The following preset inherits this preset.", count);
msg += _L_PLURAL("The following preset inherits this preset.", "The following presets inherit this preset.", count);
wxString title = _L("Delete Preset");
MessageDialog(this, msg + presets, title, wxOK | wxICON_ERROR).ShowModal();
m_selected_printer.clear();
+12
View File
@@ -276,6 +276,18 @@ namespace Slic3r
std::string connect_type = j["connect_type"].get<std::string>();
std::string bind_state = j["bind_state"].get<std::string>();
// Passive discovery must not replace another agent's saved binding.
if (get_current_printer_agent_id() == MOONRAKER_PRINTER_AGENT_ID) {
if (AppConfig* config = get_app_config()) {
const auto& machines = config->get_local_machines();
auto it = machines.find(dev_id);
if (it != machines.end() && it->second.printer_agent_id != MOONRAKER_PRINTER_AGENT_ID) {
BOOST_LOG_TRIVIAL(warning) << "Ignoring Moonraker discovery for a device owned by another agent: " << dev_id;
return;
}
}
}
if (connect_type == "farm") {
connect_type ="lan";
bind_state = "free";
+28 -20
View File
@@ -4,6 +4,7 @@
#include "HMS.hpp"
#include "I18N.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/Utils/Http.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "slic3r/Utils/NetworkAgentFactory.hpp"
@@ -67,6 +68,7 @@
#include <wx/app.h>
#include <wx/colour.h>
#include <tuple>
#include <wx/app.h>
#include <wx/dir.h>
#include <wx/string.h>
#include <wx/event.h>
@@ -528,6 +530,18 @@ bool MachineObject::HasRecentLanMessage()
return diff.count() < 5000;
}
bool MachineObject::has_access_right() const
{
return !m_access_revoked && (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID || !get_access_code().empty());
}
void MachineObject::revoke_access()
{
// UI panels retain this object; revoke its binding without invalidating their pointers.
access_code.clear();
m_access_revoked = true;
}
std::string MachineObject::get_access_code() const
{
return access_code;
@@ -536,6 +550,7 @@ std::string MachineObject::get_access_code() const
void MachineObject::set_access_code(std::string code, bool only_refresh)
{
this->access_code = code;
m_access_revoked = false;
if (only_refresh) {
AppConfig* config = m_manager ? m_manager->get_app_config() : GUI::wxGetApp().app_config;
if (config) {
@@ -549,25 +564,10 @@ void MachineObject::set_access_code(std::string code, bool only_refresh)
// session boundary, since parse_user_print_info() always overwrites their code
// fresh from the cloud API's current response, so there's no cross-agent leakage
// risk to guard against there.
if (!code.empty()) {
if (has_access_right()) {
DeviceManager::update_local_machine(*this, config);
} else {
// Only patch an existing record's code - don't persist a brand-new
// never-bound entry just because set_access_code("") was called on it.
const auto& machines = config->get_local_machines();
auto it = machines.find(get_dev_id());
if (it != machines.end()) {
BBLocalMachine local_machine = it->second;
local_machine.access_code = "";
config->update_local_machine(local_machine);
}
// Also clear the pre-scoping flat legacy key when unbinding under BBL, so an
// old BBL-era code can't silently "re-bind" this device again via
// get_access_code_with_legacy_fallback()'s legacy fallback.
if (printer_agent_id == BBL_PRINTER_AGENT_ID || printer_agent_id.empty()) {
config->erase("access_code", get_dev_id());
config->erase("user_access_code", get_dev_id());
}
config->clear_local_machine_access_code(get_dev_id(), printer_agent_id);
}
} else {
if (!code.empty())
@@ -2685,7 +2685,7 @@ void MachineObject::update_print_progress(const json& value)
int MachineObject::connect()
{
if (get_dev_ip().empty()) return -1;
if (m_access_revoked || get_dev_ip().empty()) return -1;
std::string username = m_agent ? m_agent->default_lan_username() : std::string();
std::string password = get_access_code();
@@ -2698,17 +2698,25 @@ int MachineObject::connect()
std::string host = Http::get_host_from_url(input, &port);
std::string ca_file;
if (GUI::wxGetApp().preset_bundle) {
if (wxTheApp && GUI::wxGetApp().preset_bundle) {
const auto& config = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
if (port.empty())
port = config.opt_string("printhost_port");
ca_file = config.opt_string("printhost_cafile");
// Refresh credentials from the matching preset for connections that bypass GUI_App::select_machine().
const std::string print_host = config.opt_string("print_host");
if (printer_agent_id == MOONRAKER_PRINTER_AGENT_ID && config.opt_string("printer_agent") == MOONRAKER_PRINTER_AGENT_ID &&
!print_host.empty() && dev_id_from_address(print_host, config.opt_string("printhost_port")) == get_dev_id()) {
password = config.opt_string("printhost_apikey");
if (password != get_access_code())
set_access_code(password);
}
}
if (host.empty())
host = get_dev_ip();
if (m_agent) {
try {
PrinterConnectionParams params{
+3 -1
View File
@@ -119,6 +119,7 @@ private:
std::string dev_name;
std::string dev_ip;
std::string access_code;
bool m_access_revoked = false;
// type, time stamp, delay
std::vector<std::tuple<std::string, uint64_t, uint64_t>> message_delay;
@@ -229,7 +230,8 @@ public:
std::string dev_connection_name; /* lan | eth */
/*access code*/
bool has_access_right() const { return !get_access_code().empty(); }
bool has_access_right() const;
void revoke_access();
std::string get_access_code() const;
void set_access_code(std::string code, bool only_refresh = true);
@@ -389,8 +389,8 @@ static void s_update_title(const std::shared_ptr<DevNozzleRack> rack, wgtDeviceN
wxString title = _L("Induction Hotend Rack");
if (rack && (rack->GetReadingCount() > 0))
{
wxString pending = ": " + _L("Reading") + wxString::Format(" %d/%d", rack->GetReadingIdx(), rack->GetReadingCount());
title += pending;
// TRN %d/%d is the hotend being read and the number of hotends to read.
title = wxString::Format(_L("Induction Hotend Rack: Reading %d/%d"), rack->GetReadingIdx(), rack->GetReadingCount());
}
title_label->SetLabel(title);
+2 -1
View File
@@ -385,7 +385,8 @@ void EditGCodeDialog::selection_changed(wxDataViewEvent& evt)
// Orca: move below checking for def in custom defined G-code placeholders
// This allows custom placeholders to override the default ones for this dialog
// Override custom def if selection is within the preset category
if (!def || m_params_list->GetSelectedTopLevelCategory() == "Presets") {
// The group label is translated, so compare against the translated label.
if (!def || m_params_list->GetSelectedTopLevelCategory() == _L("Presets").ToStdString()) {
const auto& full_config = wxGetApp().preset_bundle->full_config();
if (const ConfigDef* config_def = full_config.def(); config_def && config_def->has(opt_key)) {
def = config_def->get(opt_key);
+74 -165
View File
@@ -2,6 +2,7 @@
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "Field.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
#include "wxExtensions.hpp"
#include "Plater.hpp"
@@ -45,7 +46,11 @@
#include <wx/arrstr.h>
#include <wx/anybutton.h>
#include <wx/colour.h>
#include <wx/clrpicker.h>
#include <wx/button.h>
#include <wx/brush.h>
#include <wx/pen.h>
#include <optional>
#include <variant>
#include <wx/image.h>
#include <wx/dcmemory.h>
#include <wx/dc.h>
@@ -2618,92 +2623,41 @@ void PluginConfigField::msw_rescale()
void ColourPicker::BUILD()
{
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width
// An empty borderless wxButton is created with zero height on macOS. Give the bitmap
// field the same height as other parameter buttons before drawing its swatch.
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
if (m_opt.height >= 0) size.SetHeight(m_opt.height*m_em_unit);
if (m_opt.width >= 0) size.SetWidth(m_opt.width*m_em_unit);
// Validate the color
wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value : m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk()) {
clr = wxTransparentColour;
}
auto temp = new wxColourPickerCtrl(m_parent, wxID_ANY, clr, wxDefaultPosition, size);
const wxString clr_str(m_opt.type == coString ? m_opt.get_default_value<ConfigOptionString>()->value :
m_opt.get_default_value<ConfigOptionStrings>()->get_at(m_opt_idx));
auto* button = new wxButton(m_parent, wxID_ANY, wxEmptyString, wxDefaultPosition, size, wxBORDER_NONE);
window = button;
button->SetBitmapMargins(0, 0);
if (parent_is_custom_ctrl && m_opt.height < 0)
opt_height = (double)temp->GetSize().GetHeight() / m_em_unit;
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
convert_to_picker_widget(temp);
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
wxGetApp().UpdateDarkUI(temp->GetPickerCtrl());
// // recast as a wxWindow to fit the calling convention
window = dynamic_cast<wxWindow*>(temp);
temp->Bind(wxEVT_COLOURPICKER_CHANGED, ([
#ifdef __WXMSW__
temp,
#endif
this](wxCommandEvent e) {
#ifdef __WXMSW__
draw_bmp_btn(temp, temp->GetColour());
#endif
on_change_field();
}), temp->GetId());
// ORCA reset value to default on right click. previously no way to switch back on windows
temp->GetPickerCtrl()->Bind(wxEVT_RIGHT_DOWN, [this, temp](wxMouseEvent e){
#ifdef __WXMSW__
temp->SetColour(wxTransparentColour);
draw_bmp_btn(temp, wxTransparentColour);
#else
set_undef_value(temp);
#endif
on_change_field();
e.Skip();
opt_height = double(button->GetSize().GetHeight()) / m_em_unit;
button->SetFont(wxGetApp().normal_font());
if (!wxOSX) button->SetBackgroundStyle(wxBG_STYLE_PAINT);
set_value(boost::any(clr_str));
button->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
button->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& event) {
apply_user_color(wxTransparentColour);
event.Skip();
});
temp->SetToolTip(get_tooltip_text(clr_str));
// Native layouts may assign the final field size after construction.
button->Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
draw_bmp_btn();
event.Skip();
});
button->SetToolTip(get_tooltip_text(clr_str));
}
void ColourPicker::set_undef_value(wxColourPickerCtrl* field)
void ColourPicker::draw_bmp_btn()
{
field->SetColour(wxTransparentColour);
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
wxImage image(btn->GetBitmap().GetSize());
image.InitAlpha();
memset(image.GetAlpha(), 0, image.GetWidth() * image.GetHeight());
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return;
#ifdef __WXMSW__
wxGCDC dc2(dc);
#else
wxDC &dc2(dc);
#endif
dc2.SetPen(wxPen("#F1754E", 1));
const wxRect rect = wxRect(0, 0, bmp.GetWidth(), bmp.GetHeight());
dc2.DrawLine(rect.GetLeftBottom(), rect.GetTopRight());
dc.SelectObject(wxNullBitmap);
btn->SetBitmapLabel(bmp);
}
// ORCA match style with button on windows
void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
{
wxButton* btn = dynamic_cast<wxButton*>(field->GetPickerCtrl());
if (!btn->GetBitmap().IsOk()) return;
btn->SetWindowStyle(wxBORDER_NONE); // ORCA just in case to prevent any overflow
btn->SetBackgroundColour(*wxWHITE);
if (!window) return;
auto* btn = static_cast<wxButton*>(window);
if (btn->GetSize().x <= 0 || btn->GetSize().y <= 0) return;
wxGetApp().UpdateDarkUI(btn);
auto create_bitmap = [btn](const wxColour& picker_color,const wxColour& bg_color, bool focus) -> wxBitmap {
wxSize btn_sz = btn->GetSize();
wxImage image(btn_sz);
@@ -2712,11 +2666,11 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
wxBitmap bmp(std::move(image));
wxMemoryDC dc(bmp);
if (!dc.IsOk()) return bmp;
wxGCDC dc2(dc); // just use wxGCDC since bitmap button only used for windows
wxGCDC dc2(dc);
dc2.SetPen(focus ? wxPen(wxColour(StateColor::darkModeColorFor(wxColour("#009688"))), 1) : *wxTRANSPARENT_PEN);
dc2.SetBrush(wxBrush(StateColor::darkModeColorFor(bg_color)));
dc2.DrawRoundedRectangle(btn->GetRect(), btn->FromDIP(4));
dc2.DrawRoundedRectangle(wxRect(wxPoint(0, 0), btn_sz), btn->FromDIP(4));
int padding = btn->FromDIP(5);
dc2.SetPen(*wxTRANSPARENT_PEN);
@@ -2735,117 +2689,72 @@ void ColourPicker::draw_bmp_btn(wxColourPickerCtrl* field, wxColour color)
return bmp;
};
btn->SetBitmap( create_bitmap(color, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(color, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(color, wxColour("#D4D4D4"), false)); // Hover
btn->SetBitmap( create_bitmap(m_colour, wxColour("#DFDFDF"), false)); // Normal
btn->SetBitmapFocus( create_bitmap(m_colour, wxColour("#DFDFDF"), true )); // Focus
btn->SetBitmapCurrent( create_bitmap(m_colour, wxColour("#D4D4D4"), false)); // Hover
}
void ColourPicker::set_value(const boost::any& value, bool change_event)
void ColourPicker::set_value(const std::string& value, bool change_event)
{
m_disable_change_event = !change_event;
const wxString clr_str(boost::any_cast<wxString>(value));
auto field = dynamic_cast<wxColourPickerCtrl*>(window);
set_value(boost::any(wxString::FromUTF8(value)), change_event);
}
#ifdef __WXMSW__
const wxColour parsed_clr(clr_str);
wxColour clr = (clr_str.IsEmpty() || !parsed_clr.IsOk()) ? wxTransparentColour : parsed_clr;
field->SetColour(clr);
draw_bmp_btn(field, clr);
#else
wxColour clr(clr_str);
if (clr_str.IsEmpty() || !clr.IsOk())
set_undef_value(field);
else
field->SetColour(clr);
#endif
m_disable_change_event = false;
void ColourPicker::set_value(const boost::any& value, bool /*change_event*/)
{
// Like wxColourPickerCtrl::SetColour, programmatic assignment is silent.
const wxString clr_str = boost::any_cast<wxString>(value);
const wxColour parsed(clr_str);
m_colour = clr_str.IsEmpty() || !parsed.IsOk() ? wxTransparentColour : parsed;
draw_bmp_btn();
}
boost::any& ColourPicker::get_value()
{
save_colors_to_config();
auto colour = static_cast<wxColourPickerCtrl*>(window)->GetColour();
if (colour == wxTransparentColour)
m_value = std::string("");
else {
m_value = encode_color(ColorRGB(colour.Red(), colour.Green(), colour.Blue()));
}
return m_value;
m_value = m_colour == wxTransparentColour ? std::string() :
encode_color(ColorRGB(m_colour.Red(), m_colour.Green(), m_colour.Blue()));
return m_value;
}
void ColourPicker::apply_user_color(const std::optional<wxColour>& color)
{
if (!color || !color->IsOk() || *color == m_colour) return;
const std::string previous = boost::any_cast<std::string>(get_value());
m_colour = *color;
draw_bmp_btn();
if (previous != boost::any_cast<std::string>(get_value())) on_change_field();
}
void ColourPicker::msw_rescale()
{
Field::msw_rescale();
wxColourPickerCtrl* field = dynamic_cast<wxColourPickerCtrl*>(window);
auto size = wxSize(def_width_wider() * m_em_unit, -1); // ORCA match color picker width with parameters
wxSize size(def_width_wider() * m_em_unit, m_parent->FromDIP(26));
if (m_opt.height >= 0)
size.SetHeight(m_opt.height * m_em_unit);
else if (parent_is_custom_ctrl && opt_height > 0)
size.SetHeight(lround(opt_height * m_em_unit));
if (m_opt.width >= 0) size.SetWidth(m_opt.width * m_em_unit);
if (parent_is_custom_ctrl)
field->SetSize(size);
else
field->SetMinSize(size);
#ifdef __WXMSW__
draw_bmp_btn(field, field->GetColour());
#else
if (field->GetColour() == wxTransparentColour)
set_undef_value(field);
#endif
if (parent_is_custom_ctrl) window->SetSize(size);
else window->SetMinSize(size);
draw_bmp_btn();
}
void ColourPicker::sys_color_changed()
{
#ifdef _WIN32
if (wxWindow* win = this->getWindow())
if (wxColourPickerCtrl* picker = dynamic_cast<wxColourPickerCtrl*>(win)){
wxGetApp().UpdateDarkUI(picker->GetPickerCtrl(), true);
draw_bmp_btn(picker, picker->GetColour());
}
#endif
if (window) wxGetApp().UpdateDarkUI(window, true);
draw_bmp_btn();
}
void ColourPicker::on_button_click(wxCommandEvent &event) {
#if !defined(__linux__) && !defined(__LINUX__)
if (m_clrData) {
std::vector<std::string> colors = wxGetApp().app_config->get_custom_color_from_config();
for (int i = 0; i < colors.size(); i++) {
m_clrData->SetCustomColour(i, string_to_wxColor(colors[i]));
}
}
m_picker_widget->OnButtonClick(event);
#endif
}
void ColourPicker::convert_to_picker_widget(wxColourPickerCtrl *widget)
void ColourPicker::on_button_click(wxCommandEvent& /*event*/)
{
#if !defined(__linux__) && !defined(__LINUX__)
m_picker_widget = dynamic_cast<wxColourPickerWidget*>(widget->GetPickerCtrl());
if (m_picker_widget) {
m_picker_widget->Bind(wxEVT_BUTTON, &ColourPicker::on_button_click, this);
m_clrData = m_picker_widget->GetColourData();
const wxColour initial = m_colour == wxTransparentColour ? wxColour(0, 0, 0) : m_colour;
const ColorRGBA color(initial.Red(), initial.Green(), initial.Blue(), static_cast<unsigned char>(255));
ColorPickerDialog dialog(m_parent, color, {}, false, window);
if (dialog.ShowModal() == wxID_OK && dialog.selection()) {
const auto& result = std::get<ColorRGBA>(*dialog.selection());
apply_user_color(wxColour(static_cast<unsigned char>(std::lround(result.r() * 255.f)),
static_cast<unsigned char>(std::lround(result.g() * 255.f)),
static_cast<unsigned char>(std::lround(result.b() * 255.f))));
}
#endif
}
void ColourPicker::save_colors_to_config() {
#if !defined(__linux__) && !defined(__LINUX__)
if (m_clrData) {
std::vector<std::string> colors;
if (colors.size() != CUSTOM_COLOR_COUNT) {
colors.resize(CUSTOM_COLOR_COUNT);
}
for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) {
colors[i] = color_to_string(m_clrData->GetCustomColour(i));
}
wxGetApp().app_config->save_custom_color_to_config(colors);
}
#endif
}
void PointCtrl::BUILD()
+10 -24
View File
@@ -22,10 +22,10 @@
#include <boost/any.hpp>
#include "I18N.hpp"
#include <wx/colourdata.h>
#include <wx/colour.h>
#include <wx/spinctrl.h>
#include <wx/bmpcbox.h>
#include <wx/clrpicker.h>
#include <optional>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Config.hpp"
@@ -610,41 +610,27 @@ private:
};
class ColourPicker : public Field {
using Field::Field;
void set_undef_value(wxColourPickerCtrl* field);
void draw_bmp_btn(wxColourPickerCtrl* field, wxColour color);
public:
ColourPicker(const ConfigOptionDef& opt, const t_config_option_key& id) : Field(opt, id) {}
ColourPicker(wxWindow* parent, const ConfigOptionDef& opt, const t_config_option_key& id) : Field(parent, opt, id) {}
~ColourPicker() {}
wxWindow* window{ nullptr };
void BUILD() override;
void set_value(const std::string& value, bool change_event = false) {
m_disable_change_event = !change_event;
dynamic_cast<wxColourPickerCtrl*>(window)->SetColour(value);
m_disable_change_event = false;
}
void set_value(const std::string& value, bool change_event = false);
void set_value(const boost::any& value, bool change_event = false) override;
boost::any& get_value() override;
void msw_rescale() override;
void sys_color_changed() override;
void enable() override { dynamic_cast<wxColourPickerCtrl*>(window)->Enable(); }
void disable() override{ dynamic_cast<wxColourPickerCtrl*>(window)->Disable(); }
void enable() override { if (window) window->Enable(); }
void disable() override{ if (window) window->Disable(); }
wxWindow* getWindow() override { return window; }
private:
void convert_to_picker_widget(wxColourPickerCtrl *widget);
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
// An absent result is cancellation; transparent color is the undefined field.
void apply_user_color(const std::optional<wxColour>& color);
void draw_bmp_btn();
void on_button_click(wxCommandEvent& event);
wxColour m_colour = wxTransparentColour;
};
class PointCtrl : public Field {
+8 -30
View File
@@ -23,7 +23,6 @@
#include <string>
#include <tuple>
#include "EncodedFilament.hpp"
#include "FilamentBitmapUtils.hpp"
#include "GUI_App.hpp"
#include "GuiColor.hpp"
@@ -278,19 +277,6 @@ static wxBitmap create_transparent_bitmap(const wxSize& size) {
return bdc.bitmap;
}
// Sort colors by HSV values (primarily by hue, then saturation, then value)
static void sort_colors_by_hsv(std::vector<wxColour>& colors) {
if (colors.size() < 2) return;
std::sort(colors.begin(), colors.end(),
[](const wxColour& a, const wxColour& b) {
ColourHSV ha = wxColourToHSV(a);
ColourHSV hb = wxColourToHSV(b);
if (ha.h != hb.h) return ha.h < hb.h;
if (ha.s != hb.s) return ha.s < hb.s;
return ha.v < hb.v;
});
}
static wxBitmap create_single_filament_bitmap(const wxColour& color, const wxSize& size)
{
// Check if color is transparent
@@ -430,24 +416,16 @@ wxBitmap create_filament_bitmap(const std::vector<wxColour>& colors, const wxSiz
{
if (colors.empty()) return wxNullBitmap;
// Make a copy to sort without modifying original
std::vector<wxColour> sorted_colors = colors;
// Sort colors by HSV when there are 2 or more colors
if (sorted_colors.size() >= 2) {
sort_colors_by_hsv(sorted_colors);
if (force_gradient && colors.size() >= 2) {
return create_gradient_filament_bitmap(colors, size);
}
if (force_gradient && sorted_colors.size() >= 2) {
return create_gradient_filament_bitmap(sorted_colors, size);
}
switch (sorted_colors.size()) {
case 1: return create_single_filament_bitmap(sorted_colors[0], size);
case 2: return create_dual_filament_bitmap(sorted_colors[0], sorted_colors[1], size);
case 3: return create_triple_filament_bitmap(sorted_colors, size);
case 4: return create_quadruple_filament_bitmap(sorted_colors, size);
default: return create_gradient_filament_bitmap(sorted_colors, size);
switch (colors.size()) {
case 1: return create_single_filament_bitmap(colors[0], size);
case 2: return create_dual_filament_bitmap(colors[0], colors[1], size);
case 3: return create_triple_filament_bitmap(colors, size);
case 4: return create_quadruple_filament_bitmap(colors, size);
default: return create_gradient_filament_bitmap(colors, size);
}
}
+7 -3
View File
@@ -124,9 +124,13 @@ void FilamentMapManualPanel::OnTimer(wxTimerEvent &)
m_errors->Hide();
m_suggestion_panel->Hide();
} else {
m_errors->SetLabel(wxString::Format(_L("Error: %s extruder has no available %s nozzle, current group result is invalid."),
invalid_eid == 0 ? _L_CONTEXT("Left", "Nozzle position") : _L_CONTEXT("Right", "Nozzle position"),
invalid_nozzle == NozzleVolumeType::nvtStandard ? _L("Standard") : _L("High Flow")));
// [extruder][nozzle volume type]
static const char* const error_msgs[2][2] = {
{L("Error: Left extruder has no available Standard nozzle, current group result is invalid."),
L("Error: Left extruder has no available High Flow nozzle, current group result is invalid.")},
{L("Error: Right extruder has no available Standard nozzle, current group result is invalid."),
L("Error: Right extruder has no available High Flow nozzle, current group result is invalid.")}};
m_errors->SetLabel(_(error_msgs[invalid_eid == 0 ? 0 : 1][invalid_nozzle == NozzleVolumeType::nvtStandard ? 0 : 1]));
// Re-wrap: wrapping only applies to the label text present when Wrap is called,
// and the label was empty at construction time.
m_errors->Wrap(FromDIP(520));
+49 -37
View File
@@ -1,4 +1,8 @@
#include "FilamentPickerDialog.hpp"
#include "ColorPickerDialog.hpp"
#include "libslic3r/Color.hpp"
#include <optional>
#include <utility>
#include "GUI.hpp"
#include "I18N.hpp"
#include "GUI_App.hpp"
@@ -79,7 +83,8 @@ void FilamentPickerDialog::StartFlashing()
m_flash_timer->Start(50);
}
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type)
FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fila_id, const FilamentColor& fila_color, const std::string& fila_type,
std::optional<FilamentColorPickerValue> ordered_color)
: DPIDialog(parent ? parent : wxGetApp().mainframe,
wxID_ANY,
_L("Select Filament"),
@@ -92,6 +97,7 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
m_color_query = new FilamentColorCodeQuery();
m_is_data_loaded = LoadFilamentData(fila_id);
m_cur_filament_color = fila_color;
m_ordered_color = std::move(ordered_color);
wxString color_name = m_color_query->GetFilaColorName(fila_id, fila_color);
m_cur_color_name = new wxString(color_name);
@@ -441,6 +447,9 @@ wxScrolledWindow* FilamentPickerDialog::CreateColorGrid()
// Bind click
btn->Bind(wxEVT_LEFT_DOWN, [this, btn, color_code](wxMouseEvent& evt) {
m_selection_changed = true;
m_ordered_color.reset();
m_custom_selection.reset();
m_cur_filament_color = color_code->GetFilaColor();
UpdatePreview(*color_code);
UpdateButtonStates(btn);
@@ -511,24 +520,27 @@ void FilamentPickerDialog::UpdatePreview(const FilamentColorCode& color_code)
Layout();
}
void FilamentPickerDialog::UpdateCustomColorPreview(const wxColour& custom_color)
void FilamentPickerDialog::UpdateCustomColorPreview(const std::vector<wxColour>& wx_colors, bool gradient)
{
std::vector<wxColour> wx_colors = {custom_color};
// Update preview bitmap
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, false);
wxBitmap bmp = create_filament_bitmap(wx_colors, COLOR_DEMO_SIZE, gradient);
if (bmp.IsOk()) {
BOOST_LOG_TRIVIAL(debug) << "Custom color bitmap created successfully: " << bmp.GetWidth() << "x" << bmp.GetHeight();
m_color_demo->SetBitmap(bmp);
m_color_demo->SetBackgroundColour(custom_color);
m_color_demo->SetBackgroundColour(wx_colors.front());
m_color_demo->Refresh();
} else {
BOOST_LOG_TRIVIAL(error) << "Failed to create custom color bitmap";
}
// Update preview labels for custom color
m_label_preview_color->SetLabel(custom_color.GetAsString(wxC2S_HTML_SYNTAX));
wxString label;
for (const auto& color : wx_colors) {
if (!label.empty()) label += " / ";
label += color.GetAsString(wxC2S_HTML_SYNTAX);
}
m_label_preview_color->SetLabel(label);
m_label_preview_idx->SetLabel("");
Layout();
}
@@ -557,16 +569,6 @@ void FilamentPickerDialog::CreateMoreInfoButton()
m_more_btn->SetStyle(ButtonStyle::Regular, ButtonType::Expanded);
}
wxColourData FilamentPickerDialog::GetSingleColorData()
{
wxColourData data;
data.SetChooseFull(true);
if (m_cur_filament_color.ColorCount() > 0) {
data.SetColour(*m_cur_filament_color.m_colors.begin());
}
return data;
}
void FilamentPickerDialog::BindEvents()
{
// Bind mouse events for window dragging
@@ -588,27 +590,37 @@ void FilamentPickerDialog::BindEvents()
// Pause click detection while color picker is open
StopClickDetection();
wxColourData original_data = GetSingleColorData();
wxColourData result = show_sys_picker_dialog(this, original_data);
// Resume click detection after color picker closes
StartClickDetection();
// Check if user actually selected a different color
if (result.GetColour() != original_data.GetColour()) {
wxColour selected_color = result.GetColour();
// Update m_current_filament_color with the selected color
m_cur_filament_color.m_colors.clear();
m_cur_filament_color.m_colors.insert(selected_color);
m_cur_filament_color.m_color_type = FilamentColor::ColorType::SINGLE_CLR;
// Update preview
UpdateCustomColorPreview(selected_color);
// Clear currently selected button since custom color selected
UpdateButtonStates(nullptr);
FilamentColorPickerValue current;
if (m_ordered_color) {
current = *m_ordered_color;
} else {
for (const auto& color : m_cur_filament_color.m_colors)
current.colors.push_back(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString());
current.gradient = m_cur_filament_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR;
}
const auto initial = filament_color_picker_initial(current);
std::optional<ColorSelection> selected;
{
ColorPickerDialog dialog(this, initial, {true, true}, !filament_color_picker_selection(current), m_more_btn);
if (dialog.ShowModal() == wxID_OK)
selected = dialog.selection();
}
StartClickDetection();
if (!filament_color_picker_changed(initial, selected))
return;
m_custom_selection = selected;
m_ordered_color = filament_color_picker_result(*selected);
m_selection_changed = true;
std::vector<wxColour> colors;
m_cur_filament_color.m_colors.clear();
for (const auto& color : m_ordered_color->colors) {
colors.emplace_back(wxString::FromUTF8(color));
m_cur_filament_color.m_colors.insert(colors.back());
}
m_cur_filament_color.m_color_type = m_ordered_color->gradient ? FilamentColor::ColorType::GRADIENT_CLR : FilamentColor::ColorType::SINGLE_CLR;
UpdateCustomColorPreview(colors, m_ordered_color->gradient);
UpdateButtonStates(nullptr);
});
}
+10 -4
View File
@@ -5,9 +5,10 @@
#include "GUI.hpp"
#include "GUI_Utils.hpp"
#include "FilamentBitmapUtils.hpp"
#include "ColorPickerDialog.hpp"
#include <optional>
#include "Widgets/Button.hpp"
#include "EncodedFilament.hpp"
#include <wx/colourdata.h>
#include <wx/dialog.h>
#include <wx/string.h>
#include <wx/event.h>
@@ -25,10 +26,13 @@ namespace Slic3r { namespace GUI {
class FilamentPickerDialog : public DPIDialog
{
public:
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type);
FilamentPickerDialog(wxWindow *parent, const wxString &fila_id, const FilamentColor &fila_color, const std::string &fila_type,
std::optional<FilamentColorPickerValue> ordered_color = std::nullopt);
virtual ~FilamentPickerDialog();
// Public interface methods
bool HasSelectionChanged() const { return m_selection_changed; }
const std::optional<ColorSelection>& GetCustomColorSelection() const { return m_custom_selection; }
bool IsDataLoaded() const { return m_is_data_loaded; }
wxColour GetSelectedColour() const;
const FilamentColor& GetSelectedFilamentColor() const { return m_cur_filament_color; }
@@ -68,7 +72,7 @@ private:
// UI update methods
void UpdatePreview(const FilamentColorCode& filament);
void UpdateCustomColorPreview(const wxColour& custom_color);
void UpdateCustomColorPreview(const std::vector<wxColour>& colors, bool gradient);
void UpdateButtonStates(wxBitmapButton* selected_btn);
// Shaped window methods
@@ -77,7 +81,6 @@ private:
// Data loading
bool LoadFilamentData(const wxString& fila_id);
wxColourData GetSingleColorData();
// Flash effect
void StartFlashing();
@@ -98,6 +101,9 @@ private:
FilamentColorCodes* m_cur_color_codes{nullptr};
wxBitmapButton* m_cur_selected_btn{nullptr};
FilamentColor m_cur_filament_color;
std::optional<FilamentColorPickerValue> m_ordered_color;
std::optional<ColorSelection> m_custom_selection;
bool m_selection_changed = false;
// Shaped window members
wxBitmap m_shape_bmp;
+21 -19
View File
@@ -1797,7 +1797,9 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
}
else {*/
//BBS: add only gcode mode
if (m_only_gcode_in_preview) {
// When the file's own settings were loaded into the presets, the bed already is the file's, with the
// model and texture of a user or custom printer too, which the lookup below cannot resolve.
if (m_only_gcode_in_preview && !wxGetApp().plater()->gcode_settings_loaded()) {
Pointfs printable_area;
//BBS: add bed exclude area
Pointfs bed_exclude_area = Pointfs();
@@ -3976,17 +3978,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
imgui.text(_u8L("Current grouping of slice result is not optimal."));
wxString tip;
if (delta_weight_to_best >= 0 && delta_change_to_best >= 0)
tip = from_u8((boost::format(_u8L("Increase %1%g filament and %2% changes compared to optimal grouping."))
% number_format(delta_weight_to_best)
% delta_change_to_best).str());
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and %2% change compared to optimal grouping.",
"Increase %1%g filament and %2% changes compared to optimal grouping.", delta_change_to_best),
number_format(delta_weight_to_best), delta_change_to_best);
else if (delta_weight_to_best >= 0 && delta_change_to_best < 0)
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to optimal grouping."))
% number_format(delta_weight_to_best)
% std::abs(delta_change_to_best)).str());
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to optimal grouping.",
"Increase %1%g filament and save %2% changes compared to optimal grouping.", std::abs(delta_change_to_best)),
number_format(delta_weight_to_best), std::abs(delta_change_to_best));
else if (delta_weight_to_best < 0 && delta_change_to_best >= 0)
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to optimal grouping."))
% number_format(std::abs(delta_weight_to_best))
% delta_change_to_best).str());
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to optimal grouping.",
"Save %1%g filament and increase %2% changes compared to optimal grouping.", delta_change_to_best),
number_format(std::abs(delta_weight_to_best)), delta_change_to_best);
imgui.text_wrapped(tip, parent_width);
ImGui::PopStyleColor(1);
@@ -3995,17 +3997,17 @@ void GCodeViewer::render_legend_color_arr_recommen(float window_padding)
ImGui::Dummy({window_padding, window_padding});
wxString tip;
if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext >= 0)
tip = from_u8((boost::format(_u8L("Save %1%g filament and %2% changes compared to a printer with one nozzle."))
% number_format(delta_weight_to_single_ext)
% delta_change_to_single_ext).str());
tip = format_wxstr(_L_PLURAL("Save %1%g filament and %2% change compared to a printer with one nozzle.",
"Save %1%g filament and %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
number_format(delta_weight_to_single_ext), delta_change_to_single_ext);
else if (delta_weight_to_single_ext >= 0 && delta_change_to_single_ext < 0)
tip = from_u8((boost::format(_u8L("Save %1%g filament and increase %2% changes compared to a printer with one nozzle."))
% number_format(delta_weight_to_single_ext)
% std::abs(delta_change_to_single_ext)).str());
tip = format_wxstr(_L_PLURAL("Save %1%g filament and increase %2% change compared to a printer with one nozzle.",
"Save %1%g filament and increase %2% changes compared to a printer with one nozzle.", std::abs(delta_change_to_single_ext)),
number_format(delta_weight_to_single_ext), std::abs(delta_change_to_single_ext));
else if (delta_weight_to_single_ext < 0 && delta_change_to_single_ext >= 0)
tip = from_u8((boost::format(_u8L("Increase %1%g filament and save %2% changes compared to a printer with one nozzle."))
% number_format(std::abs(delta_weight_to_single_ext))
% delta_change_to_single_ext).str());
tip = format_wxstr(_L_PLURAL("Increase %1%g filament and save %2% change compared to a printer with one nozzle.",
"Increase %1%g filament and save %2% changes compared to a printer with one nozzle.", delta_change_to_single_ext),
number_format(std::abs(delta_weight_to_single_ext)), delta_change_to_single_ext);
imgui.text_wrapped(tip, parent_width);
}
+274 -71
View File
@@ -1639,7 +1639,7 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
m_shortcuts_assembly_view = {
{_L("Left mouse button"), _L("Object selection")},
{alt + _L("Left mouse button"), _L("Part selection")},
{"1~16 " + _L("number keys"), _L("Number keys can quickly change the color of objects")},
{_L("1~16 number keys"), _L("Number keys can quickly change the color of objects")},
};
}
@@ -1679,6 +1679,7 @@ GLCanvas3D::~GLCanvas3D()
m_sel_plate_toolbar.del_all_item();
m_sel_plate_toolbar.del_stats_item();
m_plate_switcher_items.clear();
}
void GLCanvas3D::post_event(wxEvent &&event)
@@ -2360,6 +2361,8 @@ void GLCanvas3D::zoom_to_gcode()
void GLCanvas3D::zoom_to_plate(int plate_idx)
{
BoundingBoxf3 box;
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE && _hides_inactive_plates())
plate_idx = REQUIRES_ZOOM_TO_CUR_PLATE;
if (plate_idx == REQUIRES_ZOOM_TO_ALL_PLATE) {
box = wxGetApp().plater()->get_partplate_list().get_bounding_box();
box.min.z() = 0.0;
@@ -2416,12 +2419,22 @@ float GLCanvas3D::get_collapse_toolbar_height() const
}
// The bottom-left corner: the 3D navigator, then the column of round canvas buttons beside it, in
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar and
// get_canvas_toolbar_right, so an overlay kept clear of the corner follows its layout.
// units of the toolbar scale. Shared by _render_3d_navigator, _render_canvas_toolbar,
// get_canvas_toolbar_right and canvas_corner_height, so an overlay kept clear of the corner follows its layout.
static constexpr float NAVIGATOR_SIZE = 128.f;
static constexpr float CANVAS_TOOLBAR_MARGIN = 10.f; // off the canvas edge, when there is no navigator
static constexpr float CANVAS_TOOLBAR_PADDING = 2.f;
static constexpr float CANVAS_TOOLBAR_BUTTON = 36.f;
static constexpr float CANVAS_TOOLBAR_SPACING = 6.f;
// What the corner takes at the left edge of the canvas: the 3D navigator, or else the canvas toolbar.
static float canvas_corner_height()
{
if (wxGetApp().show_3d_navigator())
return NAVIGATOR_SIZE;
const int buttons = wxGetApp().show_canvas_zoom_button() ? 3 : 2;
return CANVAS_TOOLBAR_MARGIN + 2.f * CANVAS_TOOLBAR_PADDING + buttons * CANVAS_TOOLBAR_BUTTON + (buttons - 1) * CANVAS_TOOLBAR_SPACING;
}
float GLCanvas3D::get_canvas_toolbar_right() const
{
@@ -2546,6 +2559,9 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
wxGetApp().imgui()->new_frame();
if (_update_inactive_plates_visibility())
scene_dirty = true;
if (m_picking_enabled && !m_benchmarking) {
if (m_rectangle_selection.is_dragging())
// picking pass using rectangle selection
@@ -3053,10 +3069,9 @@ void GLCanvas3D::ensure_on_bed(unsigned int object_idx, bool allow_negative_z)
InstancesToZMap instances_min_z;
for (GLVolume* volume : m_volumes.volumes) {
ModelObject* mo = m_model->objects[volume->object_idx()];
ModelInstance* mi = mo->instances[volume->instance_idx()];
ModelObject* mo = m_model->objects[volume->object_idx()];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -4979,7 +4994,7 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
// Don't deselect a volume if layer editing is enabled or any gizmo is active. We want the object to stay selected
// during the scene manipulation.
if (m_picking_enabled && (!any_gizmo_active || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
if (m_picking_enabled && (m_gizmos.is_allow_multi_select_parts_or_objects() || !evt.CmdDown()) && (!m_hover_volume_idxs.empty())) {
if (evt.LeftDown() && !m_hover_volume_idxs.empty()) {
int volume_idx = get_first_hover_volume_idx();
bool already_selected = m_selection.contains_volume(volume_idx);
@@ -4989,7 +5004,18 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
if (already_selected && ctrl_down)
m_selection.remove(volume_idx);
else if (alt_down && m_gizmos.is_allow_multi_select_parts_or_objects() && m_selection.get_mode() == Selection::Volume &&
m_selection.get_object_idx() == m_volumes.volumes[volume_idx]->object_idx() &&
m_selection.get_instance_idx() == m_volumes.volumes[volume_idx]->instance_idx()) {
// Alt+click on another part of the selected parts' instance adds it, and on a selected
// part removes it, unless it is the only one.
if (!already_selected)
m_selection.add_volumes(Selection::Volume, {(unsigned int) volume_idx}, false);
else if (curr_idxs.size() > 1)
m_selection.remove_volumes(Selection::Volume, {(unsigned int) volume_idx});
}
else if (alt_down) {
// Select just the clicked part, also when an object is selected.
Selection::EMode mode = Selection::Volume;
if (already_selected) {
std::vector<unsigned int> volume_idxs;
@@ -5521,7 +5547,7 @@ void GLCanvas3D::set_tooltip(const std::string& tooltip)
m_tooltip.set_text(tooltip);
}
void GLCanvas3D::do_move(const std::string& snapshot_type)
void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_move)
{
if (m_model == nullptr)
return;
@@ -5553,10 +5579,18 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
// Move instances/volumes
ModelObject* model_object = m_model->objects[object_idx];
if (model_object == nullptr)
if (model_object == nullptr)
continue;
if (selection_mode == Selection::Instance) {
if (force_volume_move || selection_mode == Selection::Volume) {
auto cur_mv = model_object->volumes[volume_idx];
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
cur_mv->set_transformation(v->get_volume_transformation());
// BBS: backup
Slic3r::save_object_mesh(*model_object);
}
}
else if (selection_mode == Selection::Instance) {
if (m_canvas_type == GLCanvas3D::ECanvasType::CanvasAssembleView) {
if ((model_object->instances[instance_idx]->get_assemble_offset() - v->get_instance_offset()).norm() > 1e-2) {
model_object->instances[instance_idx]->set_assemble_transformation(v->get_instance_transformation());
@@ -5565,14 +5599,6 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation());
}
}
else if (selection_mode == Selection::Volume) {
auto cur_mv = model_object->volumes[volume_idx];
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
cur_mv->set_transformation(v->get_volume_transformation());
// BBS: backup
Slic3r::save_object_mesh(*model_object);
}
}
object_moved = true;
model_object->invalidate_bounding_box();
@@ -5589,13 +5615,14 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
// parts moved. Notifying a plate about an instance that stayed put invalidates its slice
// result, and notifying instance 0 alone left a moved copy unregistered on its new plate.
{
const bool parts_moved = force_volume_move || selection_mode == Selection::Volume;
std::set<std::pair<int, int>> notified;
for (unsigned int i : m_selection.get_volume_idxs()) {
const GLVolume* v = m_volumes.volumes[i];
const int object_idx = v->object_idx();
if (object_idx < 0 || object_idx >= static_cast<int>(m_model->objects.size()))
continue;
const std::pair<int, int> key(object_idx, selection_mode == Selection::Volume ? -1 : v->instance_idx());
const std::pair<int, int> key(object_idx, parts_moved ? -1 : v->instance_idx());
if (notified.insert(key).second)
m_selection.notify_instance_update(key.first, key.second);
}
@@ -5604,9 +5631,8 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int>& i : done) {
ModelObject* mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -5735,9 +5761,8 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int> &i : done) {
ModelObject *mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -5838,9 +5863,8 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int>& i : done) {
ModelObject* mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -5959,9 +5983,8 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
// Fixes sinking/flying instances (snaps object to buildplate)
for (const std::pair<int, int>& i : done) {
ModelObject* mo = m_model->objects[i.first];
ModelInstance* mi = mo->instances[i.second];
if (!mi->auto_drop) {
if (!mo->auto_drop) {
continue;
}
@@ -7662,7 +7685,6 @@ void GLCanvas3D::_update_select_plate_toolbar_stats_item(bool force_selected) {
bool GLCanvas3D::_update_imgui_select_plate_toolbar()
{
bool result = true;
if (!m_sel_plate_toolbar.is_enabled()) {
return false;
}
@@ -7687,19 +7709,13 @@ bool GLCanvas3D::_update_imgui_select_plate_toolbar()
PartPlateList& plate_list = p_plater->get_partplate_list();
for (int i = 0; i < plate_list.get_plate_count(); i++) {
IMToolbarItem* item = new IMToolbarItem();
PartPlate* plate = plate_list.get_plate(i);
if (plate && plate->thumbnail_data.is_valid()) {
PartPlate* plate = plate_list.get_plate(i);
item->image_data = plate->thumbnail_data.pixels;
item->image_width = plate->thumbnail_data.width;
item->image_height = plate->thumbnail_data.height;
result = item->generate_texture();
}
if (PartPlate* plate = plate_list.get_plate(i))
item->set_thumbnail(plate->thumbnail_data);
m_sel_plate_toolbar.m_items.push_back(item);
}
p_plater->clear_plate_toolbar_image_dirty();
m_sel_plate_toolbar.is_display_scrollbar = false;
return result;
return true;
}
//BBS: GUI refactor
@@ -7967,8 +7983,8 @@ void GLCanvas3D::_picking_pass()
if (0 <= hit.raycaster_id && hit.raycaster_id < (int)m_volumes.volumes.size()) {
const GLVolume* volume = m_volumes.volumes[hit.raycaster_id];
if (volume->is_active && !volume->disabled && (volume->composite_id.volume_id >= 0 || m_render_sla_auxiliaries)) {
// do not add the volume id if any gizmo is active and CTRL is pressed
if (m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined || !wxGetKeyState(WXK_CONTROL))
// do not add the volume id if CTRL is pressed while a gizmo that does not allow Ctrl+click selection is active
if (m_gizmos.is_allow_multi_select_parts_or_objects() || !wxGetKeyState(WXK_CONTROL))
m_hover_volume_idxs.emplace_back(hit.raycaster_id);
m_gizmos.set_hover_id(-1);
}
@@ -8894,11 +8910,75 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
const Vec2d bc = m_bed.build_volume().bed_center();
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
} else {
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
// With the other plates hidden, the axes go to the current plate as in Preview.
m_bed.set_axes_mode(m_main_toolbar.is_enabled() && !_hides_inactive_plates());
}
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
}
bool GLCanvas3D::_hides_inactive_plates() const
{
return m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas && wxGetApp().focus_mode();
}
bool GLCanvas3D::_update_inactive_plates_visibility()
{
const bool hide = _hides_inactive_plates();
if (!hide && !m_inactive_plates_hidden)
return false;
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
PartPlate* plate = hide ? plate_list.get_curr_plate() : nullptr;
struct Instance
{
BoundingBoxf3 box;
bool selected{ false };
};
std::map<std::pair<int, int>, Instance> instances;
if (plate != nullptr)
for (const GLVolume* volume : m_volumes.volumes) {
Instance& instance = instances[{ volume->object_idx(), volume->instance_idx() }];
instance.box.merge(volume->transformed_convex_hull_bounding_box());
instance.selected |= volume->selected;
}
// A gizmo that hides the other objects shows them all again when it closes.
const bool keep_hidden = m_gizmos.is_hiding_instances();
bool changed = false;
bool any_hidden = false;
std::vector<bool> shown_again(m_volumes.volumes.size(), false);
for (size_t i = 0; i < m_volumes.volumes.size(); ++i) {
GLVolume* volume = m_volumes.volumes[i];
const Instance* instance = plate != nullptr ? &instances[{ volume->object_idx(), volume->instance_idx() }] : nullptr;
if (instance != nullptr && !instance->selected && instance->box.defined && !plate->get_bounding_box().overlap(instance->box)) {
changed |= volume->is_active;
volume->is_active = false;
volume->on_hidden_plate = true;
} else if (volume->on_hidden_plate && !keep_hidden) {
volume->is_active = true;
volume->on_hidden_plate = false;
shown_again[i] = true;
changed = true;
}
any_hidden |= volume->on_hidden_plate;
}
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Volume))
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters) {
const int i = SceneRaycaster::decode_id(SceneRaycaster::EType::Volume, item->get_id());
if (0 <= i && i < int(m_volumes.volumes.size()) && (m_volumes.volumes[i]->on_hidden_plate || shown_again[i]))
item->set_active(shown_again[i]);
}
if (auto* raycasters = get_raycasters_for_picking(SceneRaycaster::EType::Bed))
for (const std::shared_ptr<SceneRaycasterItem>& item : *raycasters)
item->set_active(!hide || SceneRaycaster::decode_id(SceneRaycaster::EType::Bed, item->get_id()) / PartPlate::GRABBER_COUNT ==
plate_list.get_curr_plate_index());
m_inactive_plates_hidden = hide || any_hidden;
return changed;
}
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
{
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
@@ -8907,7 +8987,7 @@ void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transfo
PartPlate* curr_plate = plate_list.get_curr_plate();
const Transform3d plate_view_matrix = m_design_canvas && curr_plate != nullptr ?
Transform3d(view_matrix * Geometry::translation_transform(-curr_plate->get_origin())) : view_matrix;
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
plate_list.render(plate_view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled, _hides_inactive_plates());
}
BoundingBoxf3 GLCanvas3D::_current_plate_box() const
@@ -9925,6 +10005,7 @@ void GLCanvas3D::_render_overlays()
//BBS: GUI refactor: GLToolbar
_render_imgui_select_plate_toolbar();
_render_plate_switcher();
_render_return_toolbar();
// BBS
//_render_view_toolbar();
@@ -10193,6 +10274,35 @@ void GLCanvas3D::_render_main_toolbar()
m_main_toolbar.render_arrow(*this, m_toolbar_highlighter.m_toolbar_item);
}
// The scrollbar of the plate toolbars of Preview and Prepare, in units of the toolbar scale.
static constexpr float PLATE_TOOLBAR_SCROLLBAR = 14.f;
// Pushes 4 colors and 1 style var.
static void push_plate_toolbar_scrollbar_style(bool is_dark, float scrollbar_size)
{
const ImVec4 grab(0.77f, 0.77f, 0.77f, is_dark ? .6f : 1.f);
// A background with opacity over the window's would make a second color.
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, grab);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
}
// The border of a hovered plate in the plate toolbars, when it is not the current one.
static ImVec4 plate_toolbar_hover_color(bool is_dark)
{
return ImVec4(0.67f, 0.67f, 0.67f, is_dark ? .6f : 1.f);
}
// Not IsWindowHovered(), which is false while a button is held down and would drop the bar.
static bool plate_toolbar_keeps_scrollbar(bool shown, float scrollbar_size)
{
const ImVec2 pos = ImGui::GetWindowPos();
return ImGui::IsMouseHoveringRect(pos, pos + ImGui::GetWindowSize() + ImVec2(shown ? scrollbar_size : 0.f, 0.f), !shown) ||
(ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow());
}
//BBS: GUI refactor: GLToolbar adjust
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
void GLCanvas3D::_render_imgui_select_plate_toolbar()
@@ -10323,16 +10433,15 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
int item_count = m_sel_plate_toolbar.m_items.size() + (m_sel_plate_toolbar.show_stats_item ? 1 : 0);
float window_height_calc = (item_count * (button_height + (margin_size + button_margin) * 2.0f) + (item_count - 1) * ImGui::GetStyle().ItemSpacing.y + ImGui::GetStyle().WindowPadding.y * 2.0f);
bool show_scroll = m_sel_plate_toolbar.is_display_scrollbar && (window_height_calc > window_height_max);
float scrollbar_size = 14.0f * f_scale;
float scrollbar_size = PLATE_TOOLBAR_SCROLLBAR * f_scale;
float window_height = std::min(window_height_calc, window_height_max);
float window_width = button_width + (margin_size + button_margin + ImGui::GetStyle().WindowPadding.x) * 2 + (show_scroll ? scrollbar_size : 0);
ImVec4 window_bg = m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f);
ImVec4 button_active = ImGuiWrapper::COL_ORCA; // ORCA: Use orca color for selected sliced plate border
ImVec4 button_hover = ImVec4(0.67f, 0.67f, 0.67, m_is_dark ? .6f : 1.0f);
ImVec4 button_hover = plate_toolbar_hover_color(m_is_dark);
ImVec4 orca_active = m_is_dark ? ImGuiWrapper::COL_ORCA_DARK : ImGuiWrapper::COL_ORCA;
ImVec4 orca_hover = m_is_dark ? ImGuiWrapper::COL_ORCA_HOVER_DARK : ImGuiWrapper::COL_ORCA_HOVER;
ImVec4 scroll_col = ImVec4(0.77f, 0.77f, 0.77f, m_is_dark ? .6f : 1.0f);
ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
float button_radius = 1.0f * f_scale;
@@ -10340,14 +10449,10 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
//use white text as the background switch to black
ImGui::PushStyleColor(ImGuiCol_Text, m_is_dark ? ImVec4(.9f, .9f, .9f, 1) : ImVec4(.3f, .3f, .3f, 1)); // ORCA Plate number text > Add support for dark mode
ImGui::PushStyleColor(ImGuiCol_WindowBg, window_bg);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f)); // ORCA using background color with opacity creates a second color. This prevents secondary color
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, scroll_col);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, scroll_col);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, scroll_col);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, button_active);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, button_hover);
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar_size);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_size);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, show_scroll ? (6.f * f_scale) : (button_radius + margin_size + frame_padding + ImGui::GetStyle().WindowPadding.x));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, button_radius + margin_size);
@@ -10618,18 +10723,109 @@ void GLCanvas3D::_render_imgui_select_plate_toolbar()
ImGui::PopStyleColor(8);
ImGui::PopStyleVar(7);
//ORCA ImGui::IsWindowHovered() returns false when left_down events on buttons that causes scrollbar disappears for a short time
auto win_pos = ImGui::GetWindowPos();
bool is_win_hovered = ImGui::IsMouseHoveringRect(win_pos, win_pos + ImVec2(window_width + (show_scroll ? scrollbar_size : 0), window_height), !show_scroll);
// Also show scrollbar visible and continue to capture mouse position
const bool is_scrollbar_active_drag = GImGui != nullptr && ImGui::GetIO().MouseDown[0] && GImGui->ActiveId != 0 && GImGui->ActiveIdWindow == ImGui::GetCurrentWindow();
m_sel_plate_toolbar.is_display_scrollbar = is_win_hovered || is_scrollbar_active_drag;
m_sel_plate_toolbar.is_display_scrollbar = plate_toolbar_keeps_scrollbar(show_scroll, scrollbar_size);
imgui.end();
}
void GLCanvas3D::_render_plate_switcher()
{
Plater* plater = wxGetApp().plater();
PartPlateList& plate_list = plater->get_partplate_list();
const int plate_count = plate_list.get_plate_count();
if (!_hides_inactive_plates() || plate_count < 2 || m_gizmos.get_current_type() != GLGizmosManager::Undefined)
return;
// The thumbnails are the ones of the Preview plate toolbar.
const unsigned version = plater->plate_toolbar_image_version();
if (m_plate_switcher_version != version || m_plate_switcher_items.size() != size_t(plate_count)) {
// Only the current plate is edited here; the others are redrawn when their thumbnails go invalid.
for (int i = 0; i < plate_count && !plater->is_gcode_3mf(); ++i)
plater->update_plate_thumbnails(i, i == plate_list.get_curr_plate_index());
m_plate_switcher_items.clear();
for (int i = 0; i < plate_count; ++i) {
m_plate_switcher_items.push_back(std::make_unique<IMToolbarItem>());
m_plate_switcher_items.back()->set_thumbnail(plate_list.get_plate(i)->thumbnail_data);
}
m_plate_switcher_version = version;
}
float sc = get_scale();
#ifdef WIN32
sc *= (float) get_dpi_for_window(wxGetApp().GetTopWindow()) / (float) DPI_DEFAULT;
#endif // WIN32
ImGuiWrapper& imgui = *wxGetApp().imgui();
const ImVec2 btn_size = ImVec2(72.f, 72.f) * sc;
const ImVec2 padding = ImVec2(8.f, 8.f) * sc;
const float spacing = 8.f * sc;
const float rounding = 4.f * sc;
const float scrollbar = PLATE_TOOLBAR_SCROLLBAR * sc;
const float top = std::max(m_main_toolbar.get_height(), m_gizmos.get_scaled_total_height()) + CANVAS_TOOLBAR_MARGIN * sc;
const float content_h = plate_count * (btn_size.y + spacing) - spacing + 2.f * padding.y;
const float max_h = std::max(btn_size.y + 2.f * padding.y, float(get_canvas_size().get_height()) - top - (canvas_corner_height() + CANVAS_TOOLBAR_MARGIN) * sc);
// As in the Preview plate toolbar, the scrollbar shows only while the mouse is over the column.
const bool scroll = m_plate_switcher_scrollbar && content_h > max_h;
const ImVec4 text_color = m_is_dark ? ImVec4(.9f, .9f, .9f, 1.f) : ImVec4(.3f, .3f, .3f, 1.f);
const ImVec4 empty_color = ImVec4(text_color.x, text_color.y, text_color.z, .45f);
const ImVec4 hover = plate_toolbar_hover_color(m_is_dark);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, padding);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
// Concentric with the thumbnails; tighter around the scrollbar, as in the Preview plate toolbar.
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, scroll ? 6.f * sc : rounding + padding.x);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, m_is_dark ? ImVec4(.13f, .13f, .15f, .5f) : ImVec4(1.f, 1.f, 1.f, .7f));
push_plate_toolbar_scrollbar_style(m_is_dark, scrollbar);
imgui.set_next_window_pos(CANVAS_TOOLBAR_MARGIN * sc, top, ImGuiCond_Always, 0.f, 0.f);
imgui.set_next_window_size(btn_size.x + 2.f * padding.x + (scroll ? scrollbar : 0.f), std::min(content_h, max_h), ImGuiCond_Always);
imgui.begin(std::string("##PlateSwitcher"), ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoCollapse | (scroll ? 0 : ImGuiWindowFlags_NoScrollbar));
const int curr_plate = plate_list.get_curr_plate_index();
ImDrawList* draw_list = ImGui::GetWindowDrawList();
const ImU32 plate_bg = m_is_dark ? IM_COL32(255, 255, 255, 10) : IM_COL32(0, 0, 0, 10);
const ImU32 plate_dim = m_is_dark ? IM_COL32(30, 30, 30, 100) : IM_COL32(0, 0, 0, 50);
for (int i = 0; i < plate_count; ++i) {
PartPlate* plate = plate_list.get_plate(i);
const bool current = i == curr_plate;
const ImVec2 pos = ImGui::GetCursorScreenPos();
ImGui::PushID(i);
const bool clicked = ImGui::InvisibleButton("##plate", btn_size);
ImGui::PopID();
const bool hovered = ImGui::IsItemHovered();
draw_list->AddRectFilled(pos, pos + btn_size, plate_bg, rounding);
if (ImTextureID texture = m_plate_switcher_items[i]->texture_id)
draw_list->AddImageRounded(texture, pos, pos + btn_size, ImVec2(0.f, 1.f), ImVec2(1.f, 0.f), IM_COL32_WHITE, rounding);
if (plate->empty())
draw_list->AddRectFilled(pos, pos + btn_size, plate_dim, rounding);
if (current || hovered) {
const float border = 2.f * sc;
draw_list->AddRect(pos - ImVec2(border, border) * .5f, pos + btn_size + ImVec2(border, border) * .5f,
ImGui::GetColorU32(current ? (hovered ? ImGuiWrapper::COL_ORCA_HOVER : ImGuiWrapper::COL_ORCA) : hover),
rounding + border * .5f, 0, border);
}
draw_list->AddText(pos + ImVec2(4.f, 2.f) * sc, ImGui::GetColorU32(plate->empty() ? empty_color : text_color), std::to_string(i + 1).c_str());
if (clicked && !current)
plater->CallAfter([i]() {
wxGetApp().plater()->select_plate(i);
wxGetApp().plater()->deselect_all();
});
if (hovered) {
const std::string name = plate->get_plate_name();
imgui.tooltip(_u8L("Plate") + " " + std::to_string(i + 1) + (name.empty() ? "" : " (" + name + ")"), ImGui::GetFontSize() * 20.f);
}
}
m_plate_switcher_scrollbar = plate_toolbar_keeps_scrollbar(scroll, scrollbar);
imgui.end();
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(5);
}
//BBS: GUI refactor: GLToolbar adjust
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-up
void GLCanvas3D::_render_assemble_view_toolbar() const
@@ -10736,7 +10932,7 @@ void GLCanvas3D::_render_canvas_toolbar()
ImVec2 btn_size = ImVec2(CANVAS_TOOLBAR_BUTTON, CANVAS_TOOLBAR_BUTTON) * sc;
ImVec2 margin = ImVec2(m_canvas_toolbar_pos[0] > 0 ? 0.f : (CANVAS_TOOLBAR_MARGIN * sc), CANVAS_TOOLBAR_MARGIN * sc);
ImVec2 spacing = ImVec2(6.f, 6.f) * sc;
ImVec2 spacing = ImVec2(CANVAS_TOOLBAR_SPACING, CANVAS_TOOLBAR_SPACING) * sc;
ImVec2 padding = ImVec2(CANVAS_TOOLBAR_PADDING, CANVAS_TOOLBAR_PADDING) * sc;
Vec2i32 pos = {
m_canvas_toolbar_pos[0] + margin.x,
@@ -10939,6 +11135,17 @@ void GLCanvas3D::_render_canvas_toolbar()
[]{wxGetApp().toggle_show_plate_gridlines();}
);
create_menu_item( _utf8(L("Focus mode")),
m_canvas_type == ECanvasType::CanvasView3D && !m_design_canvas, // work only on prepare
wxGetApp().focus_mode(),
[this]{
wxGetApp().toggle_focus_mode();
if (wxGetApp().focus_mode())
zoom_to_plate(REQUIRES_ZOOM_TO_CUR_PLATE);
m_dirty = true;
}
);
ImGui::Separator();
create_menu_item( _utf8(L("Labels")),
@@ -11195,7 +11402,7 @@ void GLCanvas3D::_render_paint_toolbar() const
if (!ImGui::IsMouseHoveringRect(left_arrow_button.Min, left_arrow_button.Max) && !ImGui::IsMouseHoveringRect(right_arrow_button.Min, right_arrow_button.Max)) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, { 20.0f * f_scale, 10.0f * f_scale });
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f * f_scale);
imgui.tooltip(_L("Shortcut Key ") + std::to_string(i + 1), ImGui::GetFontSize() * 20.0f);
imgui.tooltip(wxString::Format(_L("Shortcut Key %d"), i + 1), ImGui::GetFontSize() * 20.0f);
ImGui::PopStyleVar(2);
}
}
@@ -12086,12 +12293,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
extruder_name += (boost::format(_u8L("Tool %d"))%extruder_id).str();
}
if (error_iter->second.size() == 1) {
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
}
else {
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.")) %filaments %extruder_name %extruder_name).str();
}
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable range of the %s.",
error_iter->second.size()))) %filaments %extruder_name %extruder_name).str();
}
error = ErrorType::SLICING_LIMIT_ERROR;
break;
@@ -12139,11 +12343,9 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
}
}
std::string extruder_name = extruder_name_list[extruder_id-1];
if (error_iter->second.size() == 1) {
text += (boost::format(_u8L("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
} else {
text += (boost::format(_u8L("Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.")) % filaments % extruder_name % extruder_name).str();
}
text += (boost::format(into_u8(_L_PLURAL("Filament %s is placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
"Filaments %s are placed in the %s, but the generated G-code path exceeds the printable height of the %s.",
filament_ids.size()))) % filaments % extruder_name % extruder_name).str();
}
if (!text.empty()) {
text += "\n";
@@ -12170,7 +12372,8 @@ void GLCanvas3D::_set_warning_notification(EWarning warning, bool state)
warning += std::to_string(filament + 1);
warning += " ";
}
text = (boost::format(_u8L("Filaments %s cannot be printed directly on the surface of this plate.")) % warning).str();
text = (boost::format(into_u8(_L_PLURAL("Filament %s cannot be printed directly on the surface of this plate.",
"Filaments %s cannot be printed directly on the surface of this plate.", conflict_filament.size()))) % warning).str();
error = ErrorType::SLICING_ERROR;
break;
}
+12 -1
View File
@@ -660,6 +660,10 @@ private:
mutable GLToolbar m_main_toolbar;
mutable GLToolbar m_separator_toolbar;
mutable IMToolbar m_sel_plate_toolbar;
// Thumbnails of the plate switcher (Focus mode), refreshed when the plater's version changes.
std::vector<std::unique_ptr<IMToolbarItem>> m_plate_switcher_items;
unsigned m_plate_switcher_version{ unsigned(-1) };
bool m_plate_switcher_scrollbar{ false };
mutable GLToolbar m_assemble_view_toolbar;
mutable IMReturnToolbar m_return_toolbar;
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
@@ -778,6 +782,8 @@ private:
//BBS:add plate related logic
mutable std::vector<int> m_hover_volume_idxs;
std::vector<int> m_hover_plate_idxs;
// Some volume or plate raycaster may still be hidden with the inactive plates.
bool m_inactive_plates_hidden{ false };
// IMEX ghost hover state (plate-owned transparent ghost volumes).
int m_hover_ghost_head { -1 }; // physical head index, -1 when not hovering a ghost
int m_hover_ghost_plate { -1 }; // plate index for the hovered ghost, -1 when none
@@ -1305,7 +1311,7 @@ public:
void set_tooltip(const std::string& tooltip);
// the following methods add a snapshot to the undo/redo stack, unless the given string is empty
void do_move(const std::string& snapshot_type);
void do_move(const std::string& snapshot_type, bool force_volume_move = false);
void do_rotate(const std::string& snapshot_type);
void do_scale(const std::string& snapshot_type);
void do_center();
@@ -1527,6 +1533,9 @@ private:
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add part plate related logic
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
bool _hides_inactive_plates() const;
// Returns whether what the scene shows changed.
bool _update_inactive_plates_visibility();
// The current plate's box (XY, at z = 0); in the Design tab, its own bed's.
BoundingBoxf3 _current_plate_box() const;
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
@@ -1565,6 +1574,8 @@ private:
void _render_gizmos_overlay();
void _render_main_toolbar();
void _render_imgui_select_plate_toolbar();
// Prepare: picks the plate to show while the inactive ones are hidden.
void _render_plate_switcher();
void _render_assemble_view_toolbar() const;
void _render_return_toolbar() const;
void _render_canvas_toolbar();
+305 -101
View File
@@ -41,6 +41,7 @@
#include <boost/filesystem/exception.hpp>
#include <boost/locale/encoding_utf.hpp>
#include <boost/log/detail/native_typeof.hpp>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <cassert>
@@ -325,6 +326,13 @@ namespace GUI {
class MainFrame;
// The OrcaCloud agent behind the shared NetworkAgent, or null when networking/OrcaCloud is not
// available (e.g. stealth mode or the network plugin still loading).
static std::shared_ptr<OrcaCloudServiceAgent> get_orca_cloud_agent(NetworkAgent* agent)
{
return agent ? std::dynamic_pointer_cast<OrcaCloudServiceAgent>(agent->get_cloud_agent()) : nullptr;
}
void start_ping_test()
{
return;
@@ -1109,6 +1117,8 @@ void GUI_App::post_init()
}
this->check_new_version_sf();
if (!cw_showed)
this->check_legacy_user_filament_presets(false);
const auto cloud_provider = get_printer_cloud_provider();
if (is_user_login(cloud_provider) && !app_config->get_stealth_mode()) {
// this->check_privacy_version(0);
@@ -1147,14 +1157,8 @@ void GUI_App::post_init()
if(!m_networking_need_update && m_agent) {
m_agent->set_on_ssdp_msg_fn(
[this](std::string json_str) {
if (is_closing()) {
return;
}
GUI::wxGetApp().CallAfter([this, json_str] {
if (m_device_manager) {
m_device_manager->on_machine_alive(json_str);
}
});
if (!is_closing() && m_device_manager)
m_device_manager->on_machine_alive(json_str);
}
);
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
@@ -1812,14 +1816,8 @@ void GUI_App::restart_networking()
init_networking_callbacks();
m_agent->set_on_ssdp_msg_fn(
[this](std::string json_str) {
if (is_closing()) {
return;
}
GUI::wxGetApp().CallAfter([this, json_str] {
if (m_device_manager) {
m_device_manager->on_machine_alive(json_str);
}
});
if (!is_closing() && m_device_manager)
m_device_manager->on_machine_alive(json_str);
}
);
m_agent->set_on_http_error_fn([this](CloudEvent event, unsigned int status, std::string body) {
@@ -2272,7 +2270,9 @@ void GUI_App::init_networking_callbacks()
if (return_code == 5) {
GUI::wxGetApp().CallAfter([this, provider = event.provider] {
BOOST_LOG_TRIVIAL(info) << "logout: login expired";
this->request_user_logout(provider);
// Involuntary: the desktop lost its session on its own. Leave the webview's
// own session alone (guide §8).
this->request_user_logout(provider, /*user_initiated=*/false);
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
if (msg_dlg.ShowModal() == wxOK) {
return;
@@ -2616,7 +2616,10 @@ void GUI_App::init_download_path()
}
else {
fs::path dp(down_path);
if (!fs::exists(dp)) {
// ORCA: non-throwing probe; ec is deliberately unread, a download_path that cannot be
// probed falls back to the default like a missing one instead of failing startup.
boost::system::error_code ec;
if (!fs::exists(dp, ec)) {
std::string user_down_path = wxStandardPaths::Get().GetUserDir(wxStandardPaths::Dir_Downloads).ToUTF8().data();
app_config->set("download_path", user_down_path);
@@ -3549,6 +3552,10 @@ bool GUI_App::on_init_inner()
}
// BBS if load user preset failed
// Let the libslic3r know the callback, which will translate messages on demand.
// Installed before the presets load so that their load errors are translated too.
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
//if (loaded_preset_result != 0) {
try {
// Enable all substitutions (in both user and system profiles), but log the substitutions in user profiles only.
@@ -3566,9 +3573,6 @@ bool GUI_App::on_init_inner()
register_win32_device_notification_event();
#endif // WIN32
// Let the libslic3r know the callback, which will translate messages on demand.
Slic3r::I18N::set_translate_callback(libslic3r_translate_callback);
#if defined(__WXGTK__) && wxHAS_EGL
// Configure GL backend before any wxGLCanvas is created.
// On X11, prefer GLX for maximum driver compatibility.
@@ -3922,81 +3926,74 @@ bool GUI_App::on_init_network(bool try_backup)
if (config_version.empty()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no version configured, need to download";
m_networking_need_update = true;
} else {
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
__retry:
if (!load_agent_dll) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
if (!m_device_manager)
m_device_manager = new Slic3r::DeviceManager();
if (!m_user_manager)
m_user_manager = new Slic3r::UserManager();
return false;
}
int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
__retry:
if (!load_agent_dll) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok";
std::string loaded_version = Slic3r::NetworkAgent::get_version();
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
// Self-heal only when a genuinely different series loaded than configured (e.g. the
// configured build was unavailable and a fallback loaded). Within a series the
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
// compare series, not the raw string, and store the managed series form - a custom
// config (02.08.01_custom) keeps its own name.
std::string config_version = app_config->get_network_plugin_version();
std::string loaded_series = network_plugin_series(loaded_version);
if (network_plugin_series(config_version) != loaded_series) {
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
app_config->set_network_plugin_version(synced);
app_config->save();
std::string loaded_version = Slic3r::NetworkAgent::get_version();
if (app_config && !loaded_version.empty() && loaded_version != "00.00.00.00") {
// Self-heal only when a genuinely different series loaded than configured (e.g. the
// configured build was unavailable and a fallback loaded). Within a series the
// loaded build (02.08.01.53) differs from the series config (02.08.01) by design, so
// compare series, not the raw string, and store the managed series form - a custom
// config (02.08.01_custom) keeps its own name.
std::string config_version = app_config->get_network_plugin_version();
std::string loaded_series = network_plugin_series(loaded_version);
if (network_plugin_series(config_version) != loaded_series) {
std::string synced = is_series_managed_version(loaded_version) ? loaded_series : loaded_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": syncing config version from " << config_version
<< " to loaded " << loaded_version << " (stored as " << synced << ")";
app_config->set_network_plugin_version(synced);
app_config->save();
}
}
}
if (check_networking_version()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!bambu_source) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
m_networking_compatible = false;
if (check_networking_version()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version";
auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry();
if (!bambu_source) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": can not get bambu source module!";
m_networking_compatible = false;
if (should_load_networking_plugin) {
m_networking_need_update = true;
}
}
} else {
if (try_backup) {
int result = Slic3r::NetworkAgent::unload_network_module();
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
try_backup = false;
goto __retry;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
if (should_load_networking_plugin) {
m_networking_need_update = true;
}
}
} else {
if (try_backup) {
int result = Slic3r::NetworkAgent::unload_network_module();
BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result;
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true, config_version);
try_backup = false;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
// A failed install can leave the config naming a build that never made it to disk;
// fall back to the installed latest instead of dropping the user into the re-download
// flow. Only when the configured library is genuinely absent, though - a pinned series
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
std::string latest = get_latest_network_version();
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
config_version = latest;
app_config->set_network_plugin_version(latest);
app_config->save();
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
goto __retry;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, version dismatch, need upload network module";
if (should_load_networking_plugin) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
m_networking_need_update = true;
}
}
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
// A failed install can leave the config naming a build that never made it to disk;
// fall back to the installed latest instead of dropping the user into the re-download
// flow. Only when the configured library is genuinely absent, though - a pinned series
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
std::string latest = get_latest_network_version();
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
config_version = latest;
app_config->set_network_plugin_version(latest);
app_config->save();
load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(false, config_version);
goto __retry;
}
if (should_load_networking_plugin) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, need upload network module";
m_networking_need_update = true;
}
}
}
@@ -4024,6 +4021,11 @@ bool GUI_App::on_init_network(bool try_backup)
else
m_user_manager->set_agent(m_agent);
// A hot reload deletes the agent the task manager was built with. It lives until restart, so repoint it
// even if multi-device has since been turned off.
if (m_task_manager)
m_task_manager->set_agent(m_agent);
if (this->is_enable_multi_machine()) {
if (!m_task_manager) {
m_task_manager = new Slic3r::TaskManager(m_agent);
@@ -4228,8 +4230,10 @@ void GUI_App::switch_printer_agent()
const std::string print_host = config.opt_string("print_host");
if (!print_host.empty()) {
const std::string dev_id = MachineObject::dev_id_from_address(print_host, config.opt_string("printhost_port"));
MachineObject* sel = m_device_manager->get_selected_machine();
if (!sel || sel->get_dev_id() != dev_id)
MachineObject* sel = m_device_manager->get_selected_machine();
const bool access_code_changed = effective_agent_id == MOONRAKER_PRINTER_AGENT_ID && sel &&
sel->get_access_code() != config.opt_string("printhost_apikey");
if (!sel || sel->get_dev_id() != dev_id || access_code_changed)
select_machine(effective_agent_id);
}
}
@@ -4271,6 +4275,9 @@ void GUI_App::select_machine(const std::string& agent_id)
return;
}
std::string port = host_cfg->opt_string("printhost_port");
std::string access_code = host_cfg->opt_string("printhost_apikey");
if (access_code.empty() && agent_id != MOONRAKER_PRINTER_AGENT_ID)
access_code = "88888888";
// Generate dev_id from host and port
std::string dev_id = MachineObject::dev_id_from_address(print_host, port);
@@ -4297,11 +4304,6 @@ void GUI_App::select_machine(const std::string& agent_id)
machine.dev_ip = dev_id;
machine.dev_name = dev_id;
machine.printer_type = preset.config.opt_string("printer_model");
auto access_code = preset.config.opt_string("printhost_apikey");
// Orca expect non empty access code
if (access_code.empty()) {
access_code = "88888888";
}
existing = m_device_manager->insert_local_device(
machine, "lan", "free", "", access_code);
@@ -4311,6 +4313,9 @@ void GUI_App::select_machine(const std::string& agent_id)
return;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": created new machine dev_id=" << dev_id;
} else if (agent_id == MOONRAKER_PRINTER_AGENT_ID && existing->printer_agent_id == agent_id &&
(!existing->has_access_right() || existing->get_access_code() != access_code)) {
existing->set_access_code(access_code);
}
existing->local_use_ssl = boost::istarts_with(print_host, "https://");
@@ -5243,7 +5248,7 @@ void GUI_App::post_logout_to_webview(const std::string& provider)
}
}
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/)
void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PROVIDER*/, bool user_initiated)
{
if (m_agent && m_agent->is_user_login(provider)) {
m_agent->user_logout(true, provider);
@@ -5256,6 +5261,21 @@ void GUI_App::request_user_logout(const std::string& provider/* = ORCA_CLOUD_PRO
}
if (provider == ORCA_CLOUD_PROVIDER) {
// Invalidate any handoff still in flight, so a token minted for the signed-out
// account is never loaded into the pane.
++m_cloud_handoff_gen;
m_cloud_handoff_state.clear();
// An explicit user sign-out also ends the webview's own cloud session (guide §8).
// A session the desktop lost on its own must NOT do this: a 2FA user would otherwise
// loop through the code prompt on every desktop re-login.
if (user_initiated) {
if (auto orca = get_orca_cloud_agent(m_agent)) {
if (WebViewPanel* home = WebViewPanel::if_built())
home->cloud_signed_out(from_u8(orca->build_web_session_signout_url()));
}
}
/* delete old user settings */
bool transfer_preset_changes = false;
wxString header = _L("Some presets are modified.") + "\n" +
@@ -5377,7 +5397,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
else if (command_str.compare("homepage_logout") == 0) {
CallAfter([this] {
BOOST_LOG_TRIVIAL(info) << "logout: homepage_logout";
request_user_logout();
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
});
}
else if (command_str.compare("get_orca_login_info") == 0) {
@@ -5392,7 +5412,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
else if (command_str.compare("homepage_bambu_logout") == 0) {
CallAfter([this] {
BOOST_LOG_TRIVIAL(info) << "logout: homepage_bambu_logout";
request_user_logout(BBL_CLOUD_PROVIDER);
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/true);
});
}
else if (command_str.compare("homepage_orca_login_or_register") == 0) {
@@ -5401,7 +5421,7 @@ std::string GUI_App::handle_web_request(std::string cmd)
else if (command_str.compare("homepage_orca_logout") == 0) {
CallAfter([this] {
BOOST_LOG_TRIVIAL(info) << "logout: homepage_orca_logout";
request_user_logout(ORCA_CLOUD_PROVIDER);
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/true);
});
}
else if (command_str.compare("homepage_modeldepot") == 0) {
@@ -5413,6 +5433,20 @@ std::string GUI_App::handle_web_request(std::string cmd)
else if (command_str.compare("homepage_openproject") == 0) {
this->request_open_project({});
}
else if (command_str.compare("homepage_open_cloud") == 0) {
// Deferred: set_cloud_mode() may create a second native webview, which must
// not run inside this webview's script-message callback.
CallAfter([this]() {
if (WebViewPanel* home = WebViewPanel::if_built())
home->set_cloud_mode(true, cloud_webview_handoff());
});
}
else if (command_str.compare("homepage_close_cloud") == 0) {
CallAfter([]() {
if (WebViewPanel* home = WebViewPanel::if_built())
home->set_cloud_mode(false);
});
}
else if (command_str.compare("get_recent_projects") == 0) {
if (mainframe) {
if (WebViewPanel* home = WebViewPanel::if_built()) {
@@ -5685,7 +5719,9 @@ void GUI_App::on_http_error(wxCommandEvent &evt)
if (!provider.empty() && m_agent->is_user_login(provider)) {
if (std::chrono::steady_clock::now() - m_last_401_error_time > 30s) {
BOOST_LOG_TRIVIAL(warning) << "logout: http error 401.";
this->request_user_logout(provider);
// Involuntary: a 401 means the desktop session was rejected. Leave the
// webview's own session alone (guide §8).
this->request_user_logout(provider, /*user_initiated=*/false);
if (!m_show_http_error_msgdlg) {
MessageDialog msg_dlg(nullptr, _L("Login information expired. Please login again."), "", wxAPPLY | wxOK);
@@ -5829,6 +5865,93 @@ void GUI_App::on_update_machine_list(wxCommandEvent &evt)
}*/
}
wxString GUI_App::cloud_webview_handoff()
{
// The cloud pane loads the Start URL, not a token: the page checks the webview's own session
// and asks for a handoff itself (guide §4). Signed out, user_id is omitted and the page will
// post request_web_session, which is what opens the desktop login dialog.
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
return {};
return from_u8(orca->build_web_session_start_url());
}
void GUI_App::request_cloud_web_session(const std::string& state)
{
if (state.empty())
return;
// Only the newest request counts: a newer state supersedes any mint or login still in flight.
m_cloud_handoff_state = state;
const uint64_t gen = ++m_cloud_handoff_gen;
auto orca = get_orca_cloud_agent(m_agent);
if (!orca || !orca->is_user_login()) {
// Desktop signed out: open the desktop login dialog and stop here. On success
// on_user_login_handle reloads the Start URL, which makes the page request again; on
// cancel nothing happens (the guide's "on cancel, do nothing"). The newest state is
// already stored, so a repeat request while the dialog is open is dropped rather than
// stacking another dialog.
if (!login_dlg || !login_dlg->IsShown())
request_login(true, ORCA_CLOUD_PROVIDER);
return;
}
mint_cloud_web_session(state, gen);
}
void GUI_App::mint_cloud_web_session(const std::string& state, uint64_t gen)
{
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
return;
// The endpoint call blocks on network I/O (and possibly a cross-process token refresh), so
// it must not run on the UI thread. Hand off to a worker and marshal the token back.
boost::thread([this, orca, state, gen]() {
std::string token_hash;
std::string error_code;
unsigned int http_code = 0;
orca->request_web_session_handoff(token_hash, error_code, http_code);
if (is_closing())
return;
if (token_hash.empty()) {
// No token: 401 after retry, 409/429/503 or a transport error. The page owns its own
// "Sign-in link expired"/"Try again" UI; the desktop stays silent. Never log the body
// (it may echo account data), only the status/error code.
BOOST_LOG_TRIVIAL(warning) << "cloud web session handoff not minted - http_code=" << http_code
<< " error_code=" << error_code;
return;
}
GUI::wxGetApp().CallAfter([this, state, gen, token_hash]() {
on_cloud_web_session_minted(state, gen, token_hash);
});
}).detach();
}
void GUI_App::on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash)
{
if (is_closing())
return;
// Drop a result for a superseded request (a newer request_web_session, login or sign-out).
if (gen != m_cloud_handoff_gen || state != m_cloud_handoff_state)
return;
if (token_hash.empty())
return;
auto orca = get_orca_cloud_agent(m_agent);
if (!orca)
return;
// Load the Complete URL in the SAME webview that posted request_web_session, because the
// state lives in that webview's session storage (guide §3/§7).
if (WebViewPanel* home = WebViewPanel::if_built())
home->load_cloud_url(from_u8(orca->build_web_session_complete_url(token_hash, state)));
}
void GUI_App::on_user_login_handle(wxCommandEvent &evt)
{
if (!m_agent) { return; }
@@ -5884,6 +6007,16 @@ void GUI_App::on_user_login_handle(wxCommandEvent &evt)
if (app_config->get("sync_user_preset") == "true") {
start_sync_user_preset();
}
// Re-seed the embedded OrcaCloud pane if it is already open, so it picks up the new session
// without waiting for the next open. On a fresh login or account switch this invalidates any
// in-flight handoff for the previous account and loads the Start URL with the new user_id, so
// the page drops the old account and re-requests (guide §8).
++m_cloud_handoff_gen;
m_cloud_handoff_state.clear();
if (WebViewPanel* home = WebViewPanel::if_built())
if (home->cloud_mode_active())
home->set_cloud_mode(true, cloud_webview_handoff());
}
@@ -6816,6 +6949,71 @@ void GUI_App::reload_settings()
}
}
void GUI_App::check_legacy_user_filament_presets(bool by_user)
{
if (!by_user && app_config->get_bool("legacy_user_filament_presets_dont_remind"))
return;
const std::vector<LegacyUserFilamentPreset> legacy_presets = preset_bundle->legacy_user_filament_presets();
if (legacy_presets.empty()) {
if (by_user)
MessageDialog(mainframe, _L("All your filament presets are available on the printers they were made for."),
_L("Migrate Filament Presets"), wxOK | wxICON_INFORMATION).ShowModal();
return;
}
wxString message = _L("OrcaSlicer 2.5.0 restructured many system filament presets to fix incorrect profiles and prepare "
"for future features. As a result, the presets below no longer show on some printers they were made "
"for. Sorry for the trouble; this is a one-time fix.\n\n"
"Add those printers back to your presets now? Your presets are backed up first. You can also do this "
"later from Help > Migrate Filament Presets.") + "\n";
for (const LegacyUserFilamentPreset &legacy : legacy_presets) {
message += "\n" + format_wxstr(_L("%1% (based on %2%)"), legacy.preset, legacy.parent);
for (const std::string &printer : legacy.printers)
message += "\n " + from_u8(printer);
}
RichMessageDialog dlg(mainframe, message, _L("Migrate Filament Presets"), wxYES | wxNO | wxICON_QUESTION);
if (!by_user)
dlg.ShowCheckBox(_L("Don't remind me again"));
const bool migrate = dlg.ShowModal() == wxID_YES;
if (dlg.IsCheckBoxChecked())
app_config->set_bool("legacy_user_filament_presets_dont_remind", true);
if (!migrate)
return;
const PresetBundle::LegacyUserFilamentPresetMigration migration = preset_bundle->migrate_legacy_user_filament_presets(legacy_presets);
const std::string &backup = migration.backup;
const std::vector<std::string> &failed = migration.failed;
if (backup.empty()) {
show_error(mainframe, _L("Your presets could not be backed up, so they were not migrated."));
return;
}
if (!migration.locked) {
// The lock is also refused for a while after a miss, and when the lock file itself cannot be opened.
show_error(mainframe, _L("The user presets folder could not be locked, so nothing was migrated. Another OrcaSlicer "
"instance may be saving presets; try again later from Help > Migrate Filament Presets."));
return;
}
if (failed.size() == legacy_presets.size()) {
show_error(mainframe, format_wxstr(_L("Your filament presets could not be migrated. A backup of your presets was saved to:\n%1%"), backup));
return;
}
preset_bundle->update_compatible(PresetSelectCompatibleType::Never);
mainframe->update_side_preset_ui();
for (auto tab : tabs_list) {
tab->reload_config();
tab->update_changed_ui();
}
message = format_wxstr(_L("Your filament presets were migrated. Select them again in the filament list where you used them.\n\n"
"A backup of your presets was saved to:\n%1%"), backup);
if (!failed.empty()) {
message += "\n\n" + _L("These presets could not be migrated:");
for (const std::string &name : failed)
message += "\n" + from_u8(name);
}
MessageDialog(mainframe, message, _L("Migrate Filament Presets"), wxOK | (failed.empty() ? wxICON_INFORMATION : wxICON_WARNING)).ShowModal();
}
//BBS reload when logout
void GUI_App::remove_user_presets()
{
@@ -7952,8 +8150,11 @@ void GUI_App::on_stealth_mode_enter()
{
stop_sync_user_preset();
BOOST_LOG_TRIVIAL(info) << "logout: on_stealth_mode_enter";
request_user_logout(ORCA_CLOUD_PROVIDER);
request_user_logout(BBL_CLOUD_PROVIDER);
// Stealth mode forces a desktop logout, but the user did not choose "Sign out": guide §8 ties
// ?signout=1 to the explicit desktop sign-out only. Leave the cloud webview's own session
// alone, so it is re-checked (and re-seeded with the right account) on the next open.
request_user_logout(ORCA_CLOUD_PROVIDER, /*user_initiated=*/false);
request_user_logout(BBL_CLOUD_PROVIDER, /*user_initiated=*/false);
if (WebViewPanel* home = WebViewPanel::if_built()) {
home->SendCloudProvidersInfo();
}
@@ -8582,22 +8783,23 @@ void GUI_App::refresh_plugins()
// and report completion through the notification manager -- the speed dial needs no dialog.
std::thread([]() {
wxString error;
bool unknown_error = false;
try {
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
} catch (const std::exception& ex) {
error = from_u8(ex.what());
} catch (...) {
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
unknown_error = true; // translated in CallAfter: wx translation isn't safe off the UI thread
}
if (!wxTheApp)
return;
wxTheApp->CallAfter([error]() {
wxTheApp->CallAfter([error, unknown_error]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
if (error.IsEmpty())
if (error.IsEmpty() && !unknown_error)
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
@@ -8606,7 +8808,7 @@ void GUI_App::refresh_plugins()
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::ErrorNotificationLevel,
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), unknown_error ? _L("Unknown error") : error)));
});
}).detach();
}
@@ -10072,7 +10274,9 @@ void GUI_App::start_download(std::string url)
}
//lets always init so if the download dest folder was changed, new dest is used
boost::filesystem::path dest_folder(app_config->get("download_path"));
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder)) {
// ORCA: non-throwing probe, as in init_download_path(); ec is deliberately unread.
boost::system::error_code ec;
if (dest_folder.empty() || !boost::filesystem::is_directory(dest_folder, ec)) {
std::string msg = _u8L("Could not start URL download. Destination folder is not set. Please choose destination folder in Configuration Wizard.");
BOOST_LOG_TRIVIAL(error) << msg;
show_error(nullptr, msg);
+25 -1
View File
@@ -361,6 +361,11 @@ private:
bool m_side_popup_status{false};
bool m_show_http_error_msgdlg{false};
std::chrono::steady_clock::time_point m_last_401_error_time;
// OrcaCloud webview session handoff: the newest request_web_session state from the cloud pane
// and a generation counter so a superseded mint/login result is dropped (guide §5, "only the
// newest request counts").
std::string m_cloud_handoff_state;
uint64_t m_cloud_handoff_gen { 0 };
bool m_show_error_msgdlg{false};
wxString m_info_dialog_content;
HttpServer m_http_server;
@@ -431,6 +436,8 @@ public:
bool show_plate_gridlines() const { return app_config->get_bool("show_plate_gridlines"); }
void toggle_show_plate_gridlines() const { app_config->set_bool("show_plate_gridlines", !show_plate_gridlines()); }
bool focus_mode() const { return app_config->get_bool("focus_mode"); }
void toggle_focus_mode() const { app_config->set_bool("focus_mode", !focus_mode()); }
bool show_canvas_zoom_button() const { return app_config->get_bool("show_canvas_zoom_button"); }
void toggle_canvas_zoom_button() const { app_config->set_bool("show_canvas_zoom_button", !show_canvas_zoom_button()); }
@@ -540,7 +547,11 @@ public:
void request_user_login(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
void request_user_handle(int online_login = 0, const std::string& provider = ORCA_CLOUD_PROVIDER);
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER);
// `user_initiated` distinguishes a user sign-out from the desktop losing its session on its
// own (refresh failed, 401). Only an explicit sign-out also ends the cloud webview's own
// session; an involuntary one leaves the webview alone (guide §8). Defaults to the safe
// direction: a new caller that forgets the flag will not silently sign the webview out.
void request_user_logout(const std::string& provider = ORCA_CLOUD_PROVIDER, bool user_initiated = false);
void post_logout_to_webview(const std::string& provider);
int request_user_unbind(std::string dev_id, const std::string& provider = ORCA_CLOUD_PROVIDER);
std::string handle_web_request(std::string cmd);
@@ -556,6 +567,15 @@ public:
void on_update_machine_list(wxCommandEvent& evt);
void on_user_login(wxCommandEvent &evt);
void on_user_login_handle(wxCommandEvent& evt);
// OrcaCloud webview session handoff (guide §5/§7). cloud_webview_handoff builds the cloud
// pane's Start URL (with the desktop user_id while signed in). request_cloud_web_session is
// called from the pane when the page asks for a session; it opens the desktop login dialog if
// signed out, otherwise mints a one-time token on a worker thread and loads the Complete URL
// in the same webview.
wxString cloud_webview_handoff();
void request_cloud_web_session(const std::string& state);
void mint_cloud_web_session(const std::string& state, uint64_t gen);
void on_cloud_web_session_minted(const std::string& state, uint64_t gen, const std::string& token_hash);
void enable_user_preset_folder(bool enable);
// BBS
@@ -567,6 +587,10 @@ public:
void check_update(bool show_tips, int by_user);
void check_new_version(bool show_tips = false, int by_user = 0);
void check_new_version_sf(bool show_tips = false, int by_user = 0);
// Offers to migrate user filament presets made against an older release that lost printers their parent
// listed then. At startup it asks on every start that finds such presets, unless the user chose not to be
// reminded; by_user also reports when there is nothing to migrate.
void check_legacy_user_filament_presets(bool by_user);
bool process_network_msg(std::string dev_id, std::string msg);
void request_new_version(int by_user);
void enter_force_upgrade();
+69 -4
View File
@@ -16,6 +16,7 @@
#include "Selection.hpp"
//BBS: add partplate related logic
#include "PartPlate.hpp"
#include "Gizmos/GLGizmoAlignment.hpp"
#include "Gizmos/GLGizmoEmboss.hpp"
#include "Gizmos/GLGizmoSVG.hpp"
@@ -1672,6 +1673,8 @@ void MenuFactory::create_extra_object_menu()
append_menu_item_merge_parts_to_single_part(&m_object_menu);
// Object Center
append_menu_item_center(&m_object_menu);
// Object Align/Distribute
append_menu_item_align_distribute(&m_object_menu);
// Object Drop
append_menu_item_drop(&m_object_menu);
// Object Split
@@ -1779,6 +1782,7 @@ void MenuFactory::create_text_part_menu()
append_menu_item_fix_through_cgal(menu);
append_menu_item_simplify(menu);
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_items_mirror(menu);
menu->AppendSeparator();
append_menu_item_per_object_process(menu);
@@ -1811,6 +1815,7 @@ void MenuFactory::create_bbl_part_menu()
append_menu_item_simplify(menu);
append_menu_item_smooth_mesh(menu);
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_items_mirror(menu);
wxMenu* split_menu = new wxMenu();
@@ -2136,6 +2141,7 @@ wxMenu* MenuFactory::multi_selection_menu()
index++;
}
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2144,13 +2150,13 @@ wxMenu* MenuFactory::multi_selection_menu()
append_menu_item_set_printable(menu);
menu->AppendSeparator();
append_menu_item_set_auto_drop(menu);
menu->AppendSeparator();
append_menu_item_per_object_process(menu);
menu->AppendSeparator();
append_menu_items_convert_unit(menu);
append_menu_item_replace_all_with_stl(menu);
//BBS
@@ -2161,6 +2167,7 @@ wxMenu* MenuFactory::multi_selection_menu()
}
else {
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2325,6 +2332,64 @@ void MenuFactory::append_menu_item_center(wxMenu* menu)
}, m_parent);
}
void MenuFactory::append_menu_item_align_distribute(wxMenu *menu)
{
using AlignType = GLGizmoAlignment::AlignType;
auto can = [](AlignType type) {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
return type >= AlignType::DISTRIBUTE_X ? alignment_helper.can_distribute(type) : alignment_helper.can_align(type);
};
auto run = [](AlignType type) {
GLGizmoAlignment alignment_helper(*plater()->get_view3D_canvas3D());
if (type >= AlignType::DISTRIBUTE_X)
alignment_helper.distribute_objects(type);
else
alignment_helper.align_objects(type);
};
struct AlignMenuItem
{
wxString label;
AlignType type;
std::string icon;
};
const AlignMenuItem items[] = {
// icons shared with caanvas. BitmapCache converts colors dark to light. so dark variations used for proper colors
{_L("Distribute left-right") + " (X)" , AlignType::DISTRIBUTE_X, "distribute_x_dark"},
{_L("Distribute front-back") + " (Y)" , AlignType::DISTRIBUTE_Y, "distribute_y_dark"},
{_L("Distribute top-bottom") + " (Z)" , AlignType::DISTRIBUTE_Z, "distribute_z_dark"},
{wxString(), AlignType::NONE, ""},
{_L("Align left") + " (-X)", AlignType::X_MIN, "align_x_min_dark"},
{_L("Align left-right center") + " (X)" , AlignType::CENTER_X, "align_x_center_dark"},
{_L("Align right") + " (+X)", AlignType::X_MAX, "align_x_max_dark"},
{wxString(), AlignType::NONE, ""},
{_L("Align front") + " (-Y)", AlignType::Y_MIN, "align_y_min_dark"},
{_L("Align front-back center") + " (Y)" , AlignType::CENTER_Y, "align_y_center_dark"},
{_L("Align back") + " (+Y)", AlignType::Y_MAX, "align_y_max_dark"},
{wxString(), AlignType::NONE, ""},
{_L("Align bottom") + " (-Z)", AlignType::Z_MIN, "align_z_min_dark"},
{_L("Align top-bottom center") + " (Z)" , AlignType::CENTER_Z, "align_z_center_dark"},
{_L("Align top") + " (+Z)", AlignType::Z_MAX, "align_z_max_dark"},
};
wxMenu* align_distribute_menu = new wxMenu();
for (const AlignMenuItem &item : items) {
const AlignType type = item.type;
if (type == AlignType::NONE)
align_distribute_menu->AppendSeparator();
else
append_menu_item(
align_distribute_menu, wxID_ANY, item.label, "", [run, type](wxCommandEvent &) { run(type); }, item.icon, nullptr,
[can, type]() { return can(type); }, m_parent);
}
// Anything that can be distributed can also be aligned, and aligning in X has no extra condition.
append_submenu(menu, align_distribute_menu, wxID_ANY, _L("Align/Distribute"), _L("Align and distribute objects"), "",
[can]() { return can(AlignType::X_MIN); }, m_parent);
}
void MenuFactory::append_menu_item_drop(wxMenu* menu)
{
append_menu_item(menu, wxID_ANY, _L("Drop") , "",
@@ -2335,7 +2400,7 @@ void MenuFactory::append_menu_item_drop(wxMenu* menu)
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
else {
return (std::abs(plater()->get_view3D_canvas3D()->get_selection().get_bounding_box().min.z()) > -SINKING_Z_THRESHOLD);
return plater()->get_view3D_canvas3D()->get_selection().can_drop();
} //disable if model is on the bed / not in View3D
}, m_parent);
}
+1
View File
@@ -175,6 +175,7 @@ private:
void append_menu_item_merge_to_single_object(wxMenu* menu);
void append_menu_item_merge_parts_to_single_part(wxMenu *menu);
void append_menu_items_mirror(wxMenu *menu);
void append_menu_item_align_distribute(wxMenu *menu);
void append_menu_item_precise_seam_submenu(wxMenu* menu);
void append_menu_item_invalidate_cut_info(wxMenu *menu);
void append_menu_item_edit_text(wxMenu *menu);
+5 -13
View File
@@ -3234,8 +3234,8 @@ void ObjectList::merge(bool to_multipart_object)
new_object->printable = false;
new_object->instances[0]->printable = false;
}
if (object->instances[0]->auto_drop == false) {
new_object->instances[0]->auto_drop = false;
if (object->auto_drop == false) {
new_object->auto_drop = false;
}
// merge layers
@@ -7098,18 +7098,10 @@ void ObjectList::toggle_auto_drop()
obj_idxs.emplace_back(static_cast<size_t>(obj_idx));
ItemType type = m_objects_model->GetItemType(item);
// set auto_drop value for selected instance/instances in object
if (type == itInstance) {
int inst_idx = m_objects_model->GetInstanceIdByItem(item);
obj->instances[inst_idx]->auto_drop = !current_auto_drop;
} else {
for (auto inst : obj->instances)
inst->auto_drop = !current_auto_drop;
if (current_auto_drop == false)
// auto_drop is shared by all instances of an object.
obj->auto_drop = !current_auto_drop;
if (current_auto_drop == false)
obj->ensure_on_bed();
}
}
sort(obj_idxs.begin(), obj_idxs.end());
+1 -1
View File
@@ -386,7 +386,7 @@ void GridCellFilamentsRenderer::Draw(wxGrid &grid, wxGridCellAttr &attr, wxDC &d
dc.DrawBitmap(*bitmap, wxPoint(rect.x + offset_x, rect.y + offset_y));
}
else if (grid_row->model_volume_type == ModelVolumeType::PARAMETER_MODIFIER){
dc.DrawText("Default", wxPoint(rect.x + offset_x, rect.y + offset_y));
dc.DrawText(_L("Default"), wxPoint(rect.x + offset_x, rect.y + offset_y));
}
text_rect.x += bitmap_width + grid_cell_border_width * 2;
+717
View File
@@ -0,0 +1,717 @@
#include "GLGizmoAlignment.hpp"
#include "slic3r/GUI/3DScene.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Selection.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <string>
#include <vector>
namespace Slic3r {
namespace GUI {
GLGizmoAlignment::GLGizmoAlignment(GLCanvas3D& canvas) : m_canvas(canvas)
{
}
bool GLGizmoAlignment::align_objects(AlignType type, bool align_parent)
{
if (!can_align(type, align_parent)) {
return false;
}
switch (type) {
case AlignType::CENTER_X: return align_to_center(type, align_parent);
case AlignType::CENTER_Y: return align_to_center(type, align_parent);
case AlignType::CENTER_Z: return align_to_center(type, align_parent);
case AlignType::Y_MAX: return align_to_y_max(align_parent);
case AlignType::Y_MIN: return align_to_y_min(align_parent);
case AlignType::X_MAX: return align_to_x_max(align_parent);
case AlignType::X_MIN: return align_to_x_min(align_parent);
case AlignType::Z_MAX: return align_to_z_max(align_parent);
case AlignType::Z_MIN: return align_to_z_min(align_parent);
default:
return false;
}
}
bool GLGizmoAlignment::distribute_objects(AlignType type)
{
if (!can_distribute(type)) {
return false;
}
switch (type) {
case AlignType::DISTRIBUTE_X:
return distribute_x();
case AlignType::DISTRIBUTE_Y:
return distribute_y();
case AlignType::DISTRIBUTE_Z:
return distribute_z();
default:
return false;
}
}
bool GLGizmoAlignment::align_to_y_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.y(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.y() = target - current_max;
}, "Align Y Max", align_parent
);
}
bool GLGizmoAlignment::align_to_y_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.y(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.y() = target - current_min;
}, "Align Y Min", align_parent
);
}
bool GLGizmoAlignment::align_to_x_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.x(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.x() = target - current_max;
}, "Align X Max", align_parent
);
}
bool GLGizmoAlignment::align_to_x_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.x(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.x() = target - current_min;
}, "Align X Min", align_parent
);
}
bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent)
{
std::string type_str = type == AlignType::CENTER_X ? "X" : (type == AlignType::CENTER_Y ? "Y" : "Z");
std::string operation_name = type == AlignType::CENTER_X ? "X Center" : (type == AlignType::CENTER_Y ? "Y Center" : "Z Center");
double selection_center = 0.f;
if (align_parent) {
BoundingBoxf3 parent_box = get_parent_box();
selection_center = get_current_coord(operation_name, parent_box);
// Handle multiple volume selection
if (is_parts_selection(true)) {
if (!take_snapshot("Align to parent center " + type_str)) {
return false;
}
get_selection().setup_cache();
BoundingBoxf3 selection_bbox = get_items_box();
double current_coord = get_current_coord(operation_name, selection_bbox);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
// Move the selected volumes together so that their collective bounding box is centered in the parent
for (const GLVolume *volume : get_selected_part_volumes()) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
finish_operation(true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
// Handle multiple objects or parts selection
if (!take_snapshot("Align to parent center " + type_str)) {
return false;
}
get_selection().setup_cache();
double current_coord = get_current_coord(operation_name, big_bb);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
// Multiple objects: use selection bounding box for alignment
for (const auto &obj : objects) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
finish_operation();
return true;
}
// Handle the parts of a single instance
if (is_parts_selection(false)) {
if (!take_snapshot("Align Parts Center " + type_str)){
return false;
}
get_selection().setup_cache();
BoundingBoxf3 selection_bbox = get_items_box();
selection_center = get_current_coord(operation_name, selection_bbox);
// Move each part to align to the center of their collective bounding box
for (const GLVolume *volume : get_selected_part_volumes()) {
auto temp_box = volume->transformed_convex_hull_bounding_box();
double current = get_current_coord(operation_name, temp_box);
double offset = selection_center - current;
Vec3d displacement = generate_displacement(operation_name, offset);
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
finish_operation(true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
// Handle multiple instances
selection_center = get_current_coord(operation_name, big_bb);
if (!take_snapshot("Align Objects Center" + type_str)) {
return false;
}
get_selection().setup_cache();
// Multiple objects: use selection bounding box for alignment
for (const auto& obj : objects) {
BoundingBoxf3 temp_bbox = obj.bbox;
double current_coord = get_current_coord(operation_name, temp_bbox);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
finish_operation();
return true;
}
bool GLGizmoAlignment::align_to_z_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.z(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.z() = target - current_max;
}, "Align Z Max", align_parent
);
}
bool GLGizmoAlignment::align_to_z_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.z(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.z() = target - current_min;
}, "Align Z Min", align_parent
);
}
bool GLGizmoAlignment::distribute_y()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.y(); },
1,
"Distribute Y"
);
}
bool GLGizmoAlignment::distribute_x()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.x(); },
0,
"Distribute X"
);
}
bool GLGizmoAlignment::distribute_z()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.z(); },
2,
"Distribute Z"
);
}
template<typename GetCoordFunc, typename SetCoordFunc>
bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name ,bool align_parent)
{
if (align_parent) {
// Find reference coordinate
BoundingBoxf3 parent_box = get_parent_box();
double reference_coord = get_current_coord(operation_name, parent_box);
// Handle parts selection differently
if (is_parts_selection(true)) {
if (!take_snapshot("align to parent node " + operation_name)) {
return false;
}
get_selection().setup_cache();
const std::vector<const GLVolume *> volumes = get_selected_part_volumes();
if (volumes.empty())
return false;
BoundingBoxf3 selection_bbox = get_items_box();
double current_coord = get_current_coord(operation_name, selection_bbox);
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
for (const GLVolume *volume : volumes) {
if (displacement.norm() > 1e-6) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
}
finish_operation(true);
return true;
}
// Handle objects selection (original logic)
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
if (!take_snapshot("align to parent node " + operation_name)) { return false; }
get_selection().setup_cache();
double current_coord = get_current_coord(operation_name, big_bb);
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
for (const auto &obj : objects) {
if (displacement.norm() > 1e-6) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
}
finish_operation();
return true;
}
// Handle parts selection differently
if (is_parts_selection(false)) {
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
if (volumes.empty()) return false;
// Find reference coordinate
double reference_coord = 0.0;
bool first = true;
for (const GLVolume* volume : volumes) {
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
double coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
coord = bbox.min.z();
}
if (first) {
reference_coord = coord;
first = false;
} else {
if (operation_name.find("Max") != std::string::npos) {
reference_coord = std::max(reference_coord, coord);
} else if (operation_name.find("Min") != std::string::npos) {
reference_coord = std::min(reference_coord, coord);
}
}
}
for (const GLVolume* volume : volumes) {
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
double current_coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
current_coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
current_coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
current_coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
current_coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
current_coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
current_coord = bbox.min.z();
}
Vec3d displacement = Vec3d::Zero();
if (operation_name.find("X") != std::string::npos) {
displacement.x() = reference_coord - current_coord;
} else if (operation_name.find("Y") != std::string::npos) {
displacement.y() = reference_coord - current_coord;
} else if (operation_name.find("Z") != std::string::npos) {
displacement.z() = reference_coord - current_coord;
}
if (displacement.norm() > 1e-6) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement, false);
}
}
finish_operation(true);
return true;
}
// Handle objects selection (original logic)
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
double reference_coord = get_coord(objects[0]);
for (const auto& obj : objects) {
double coord = get_coord(obj);
if (operation_name.find("Max") != std::string::npos) {
reference_coord = std::max(reference_coord, coord);
} else if (operation_name.find("Min") != std::string::npos) {
reference_coord = std::min(reference_coord, coord);
}
}
for (const auto& obj : objects) {
double current_coord = get_coord(obj);
Vec3d displacement = Vec3d::Zero();
set_coord(displacement, reference_coord, current_coord);
if (displacement.norm() > 1e-6) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
}
finish_operation();
return true;
}
template<typename GetCoordFunc>
bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int axis, const std::string& operation_name)
{
if (is_parts_selection(false)) {
const std::vector<const GLVolume*> volumes = get_selected_part_volumes();
if (volumes.size() < 3) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
struct VolEntry { const GLVolume* v; double coord; };
std::vector<VolEntry> entries;
entries.reserve(volumes.size());
for (const GLVolume* v : volumes) {
BoundingBoxf3 bbox = v->transformed_convex_hull_bounding_box();
Vec3d c = bbox.center();
double coord = (axis == 0 ? c.x() : axis == 1 ? c.y() : c.z());
entries.push_back({v, coord});
}
std::sort(entries.begin(), entries.end(), [](const VolEntry& a, const VolEntry& b){ return a.coord < b.coord; });
BoundingBoxf3 selection_bbox = get_items_box();
BoundingBoxf3 left_bbox = entries.front().v->transformed_convex_hull_bounding_box();
BoundingBoxf3 right_bbox = entries.back().v->transformed_convex_hull_bounding_box();
auto axis_extent = [&](const BoundingBoxf3 &bb) { return (axis == 0 ? (bb.max.x() - bb.min.x()) : axis == 1 ? (bb.max.y() - bb.min.y()) : (bb.max.z() - bb.min.z())); };
double left_width = axis_extent(left_bbox);
double right_width = axis_extent(right_bbox);
double bbox_min = (axis == 0 ? selection_bbox.min.x() : axis == 1 ? selection_bbox.min.y() : selection_bbox.min.z());
double bbox_max = (axis == 0 ? selection_bbox.max.x() : axis == 1 ? selection_bbox.max.y() : selection_bbox.max.z());
double min_center = bbox_min + left_width * 0.5;
double max_center = bbox_max - right_width * 0.5;
double total_distance = max_center - min_center;
double interval = (entries.size() > 1) ? total_distance / (entries.size() - 1) : 0;
for (size_t i = 1; i < entries.size() - 1; ++i) {
double target_coord = min_center + i * interval;
double current_coord = entries[i].coord;
double offset = target_coord - current_coord;
if (std::abs(offset) > 1e-6) {
Vec3d displacement = Vec3d::Zero();
displacement[axis] = offset;
const GLVolume *v = entries[i].v;
get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement, false);
}
}
finish_operation(true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.size() < 3) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
std::sort(objects.begin(), objects.end(),
[&get_coord](const ObjectInfo& a, const ObjectInfo& b) {
return get_coord(a) < get_coord(b);
});
double min_coord = get_coord(objects.front());
double max_coord = get_coord(objects.back());
double total_distance = max_coord - min_coord;
double interval = (objects.size() > 1) ? total_distance / (objects.size() - 1) : 0;
for (size_t i = 1; i < objects.size() - 1; ++i) {
double target_coord = min_coord + i * interval;
double current_coord = get_coord(objects[i]);
double offset = target_coord - current_coord;
if (std::abs(offset) > 1e-6) {
Vec3d displacement = Vec3d::Zero();
displacement[axis] = offset;
apply_transformation(objects[i].object_idx, objects[i].instance_idx, displacement);
}
}
finish_operation();
return true;
}
bool GLGizmoAlignment::can_align(AlignType type, bool align_parent) const
{
return get_items_count(type, align_parent) >= (align_parent ? 1 : 2);
}
bool GLGizmoAlignment::can_distribute(AlignType type) const
{
return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z) &&
get_items_count(type, false) >= 3;
}
double GLGizmoAlignment::get_current_coord(std::string operation_name, BoundingBoxf3 &bbox)
{
double current_coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
current_coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
current_coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
current_coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
current_coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
current_coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
current_coord = bbox.min.z();
} else if (operation_name.find("X Center") != std::string::npos) {
current_coord = bbox.center().x();
} else if (operation_name.find("Y Center") != std::string::npos) {
current_coord = bbox.center().y();
} else if (operation_name.find("Z Center") != std::string::npos) {
current_coord = bbox.center().z();
}
return current_coord;
}
Vec3d GLGizmoAlignment::generate_displacement(std::string operation_name, double offset) {
Vec3d displacement = Vec3d::Zero();
if (operation_name.find("X") != std::string::npos) {
displacement.x() = offset;
} else if (operation_name.find("Y") != std::string::npos) {
displacement.y() = offset;
} else if (operation_name.find("Z") != std::string::npos) {
displacement.z() = offset;
}
return displacement;
}
std::vector<GLGizmoAlignment::ObjectInfo> GLGizmoAlignment::get_selected_objects_info(BoundingBoxf3 &big_bb) const
{
std::vector<ObjectInfo> objects;
Selection& selection = get_selection();
if (selection.is_empty()) return objects;
const auto& content = selection.get_content();
for (const auto& obj_it : content) {
for (const auto& inst_idx : obj_it.second) {
int obj_idx = obj_it.first;
if (obj_idx >= 0 && obj_idx < selection.get_model()->objects.size()) {
ModelObject* object = selection.get_model()->objects[obj_idx];
if (inst_idx >= 0 && inst_idx < object->instances.size()) {
BoundingBoxf3 bbox = object->instance_bounding_box(inst_idx);
big_bb.merge(bbox);
objects.emplace_back(obj_idx, inst_idx, bbox);
}
}
}
}
return objects;
}
bool GLGizmoAlignment::is_parts_selection(bool align_parent) const
{
const Selection &selection = get_selection();
const auto &content = selection.get_content();
if (content.size() != 1 || content.begin()->first >= 1000 || content.begin()->second.size() != 1)
return false;
return !align_parent || selection.get_mode() == Selection::Volume;
}
std::vector<const GLVolume *> GLGizmoAlignment::get_selected_part_volumes() const
{
std::vector<const GLVolume *> volumes;
if (!is_parts_selection(false))
return volumes;
const Selection &selection = get_selection();
const ModelObject *object = selection.get_model()->objects[selection.get_object_idx()];
if (object->is_cut())
return volumes;
for (unsigned int idx : selection.get_volume_idxs()) {
const GLVolume *volume = selection.get_volume(idx);
if (volume->volume_idx() < 0)
continue;
// Moving a whole instance's parts leaves its modifiers, negative volumes and support blockers in place,
// as dragging a part does.
if (selection.get_mode() == Selection::Volume || object->volumes[volume->volume_idx()]->is_model_part())
volumes.push_back(volume);
}
return volumes;
}
BoundingBoxf3 GLGizmoAlignment::get_items_box() const
{
BoundingBoxf3 box;
if (is_parts_selection(false)) {
for (const GLVolume *volume : get_selected_part_volumes())
box.merge(volume->transformed_convex_hull_bounding_box());
} else
get_selected_objects_info(box);
return box;
}
BoundingBoxf3 GLGizmoAlignment::get_parent_box() const
{
const Selection &selection = get_selection();
if (is_parts_selection(true))
return selection.get_model()->objects[selection.get_object_idx()]->instance_convex_hull_bounding_box(selection.get_instance_idx());
// can_align() requires every selected instance on one plate, so the first one gives the plate.
BoundingBoxf3 box;
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
if (obj_idx >= 1000 || inst_idxs.empty())
continue;
PartPlate *plate = plate_list.get_plate(plate_list.find_instance(obj_idx, *inst_idxs.begin()));
if (plate == nullptr)
break;
BoundingBoxf3 instances_box;
get_selected_objects_info(instances_box);
box = plate->get_plate_box();
box.max.z() = instances_box.max.z();
// Keep the aligned objects off the plate edge.
box.min += Vec3d(0.1, 0.1, 0.1);
box.max -= Vec3d(0.1, 0.1, 0.1);
break;
}
return box;
}
Selection& GLGizmoAlignment::get_selection() const
{
return m_canvas.get_selection();
}
bool GLGizmoAlignment::take_snapshot(const std::string& name)
{
wxGetApp().plater()->take_snapshot(name);
return true;
}
void GLGizmoAlignment::apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement)
{
get_selection().translate(obj_idx, inst_idx, displacement);
}
void GLGizmoAlignment::finish_operation(bool force_volume_move)
{
// A part move changes the ModelVolume shared by every copy of the object, so the other copies' volumes must show
// it too, or do_move() would write their stale transformation back to the model.
if (force_volume_move)
get_selection().synchronize_unselected_volumes();
// The operation already took its snapshot.
m_canvas.do_move("", force_volume_move);
}
size_t GLGizmoAlignment::get_items_count(AlignType type, bool align_parent, std::string *why_not) const
{
auto cannot_move = [why_not](const std::string &reason) {
if (why_not != nullptr)
*why_not = reason;
return size_t(0);
};
if (is_parts_selection(align_parent)) {
const size_t count = get_selected_part_volumes().size();
return count > 0 ? count : cannot_move(_u8L("Parts of a cut object cannot be moved"));
}
// Instances are moved through their instance offsets, which do_move() commits only from an Instance selection.
const Selection &selection = get_selection();
if (selection.get_mode() != Selection::Instance)
return 0;
const bool is_z = type == AlignType::Z_MIN || type == AlignType::CENTER_Z || type == AlignType::Z_MAX ||
type == AlignType::DISTRIBUTE_Z;
// The plate gives no Z target, and aligning to its bottom would duplicate Drop.
if (is_z && align_parent)
return cannot_move(_u8L("Aligning to the plate is available in X and Y only"));
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
int plate_idx = -1;
size_t count = 0;
for (const auto &[obj_idx, inst_idxs] : selection.get_content()) {
if (obj_idx >= 1000)
continue;
// Auto-drop would put a raised object back on the bed and leave a lowered one sunk.
if (is_z && selection.get_model()->objects[obj_idx]->auto_drop)
return cannot_move(_u8L("Turn off Auto-Drop for the selected objects to move them in Z"));
if (align_parent) {
// The plate holding the selection is the target, so it must hold all of it.
for (int inst_idx : inst_idxs) {
const int idx = plate_list.find_instance(obj_idx, inst_idx);
if (idx == -1 || (plate_idx != -1 && idx != plate_idx))
return cannot_move(_u8L("All selected objects must be on the same plate"));
plate_idx = idx;
}
}
count += inst_idxs.size();
}
return count;
}
} // namespace GUI
} // namespace Slic3r
+102
View File
@@ -0,0 +1,102 @@
#ifndef slic3r_GLGizmoAlignment_hpp_
#define slic3r_GLGizmoAlignment_hpp_
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "slic3r/GUI/Selection.hpp"
#include <cstddef>
#include <string>
#include <vector>
namespace Slic3r {
namespace GUI {
class GLCanvas3D;
class GLGizmoAlignment
{
public:
enum class AlignType {
NONE = -1,
CENTER_X,
CENTER_Y,
CENTER_Z,
Y_MAX,
Y_MIN,
X_MAX,
X_MIN,
Z_MAX,
Z_MIN,
DISTRIBUTE_X,
DISTRIBUTE_Y,
DISTRIBUTE_Z
};
struct ObjectInfo {
int object_idx;
int instance_idx;
BoundingBoxf3 bbox;
Vec3d center;
ObjectInfo(int obj_idx, int inst_idx, const BoundingBoxf3& bb)
: object_idx(obj_idx), instance_idx(inst_idx), bbox(bb), center(bb.center()) {}
};
explicit GLGizmoAlignment(GLCanvas3D& canvas);
~GLGizmoAlignment() = default;
bool align_objects(AlignType type,bool align_parent = false);
bool distribute_objects(AlignType type);
bool align_to_center(AlignType type,bool align_parent = false);
bool align_to_y_max(bool align_parent = false);
bool align_to_y_min(bool align_parent = false);
bool align_to_x_max(bool align_parent = false);
bool align_to_x_min(bool align_parent = false);
bool align_to_z_max(bool align_parent = false);
bool align_to_z_min(bool align_parent = false);
bool distribute_x();
bool distribute_y();
bool distribute_z();
bool can_align(AlignType type, bool align_parent = false) const;
bool can_distribute(AlignType type) const;
std::vector<ObjectInfo> get_selected_objects_info(BoundingBoxf3 &big_bb) const;
double get_current_coord(std::string operation_name, BoundingBoxf3 &bbox);
Vec3d generate_displacement(std::string operation_name, double offset);
// A parts selection (one instance of one object) aligns the parts inside it; anything else aligns whole instances.
// With align_parent, only a Volume-mode selection counts as parts: a whole instance aligns to its plate instead.
bool is_parts_selection(bool align_parent) const;
// The volumes a parts operation moves: the selected volumes, or only the model parts when a whole instance is
// selected. Empty for a cut object, whose parts must keep their place.
std::vector<const GLVolume *> get_selected_part_volumes() const;
// World bounding box of the items an operation moves, the reference of every non-parent operation.
BoundingBoxf3 get_items_box() const;
// World bounding box aligned to with align_parent: the instance of a parts selection, or the plate holding the
// selected instances, up to their top.
BoundingBoxf3 get_parent_box() const;
// Number of items the operation would move, or 0 when it cannot move them; why_not then gets the reason, if any.
size_t get_items_count(AlignType type, bool align_parent, std::string *why_not = nullptr) const;
private:
GLCanvas3D& m_canvas;
template<typename GetCoordFunc, typename SetCoordFunc>
bool align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name, bool align_parent = false);
template<typename GetCoordFunc>
bool distribute_objects_generic(GetCoordFunc get_coord, int axis,
const std::string& operation_name);
Selection& get_selection() const;
bool take_snapshot(const std::string& name);
void apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement);
void finish_operation(bool force_volume_move = false);
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoAlignment_hpp_
Loaded 100 of 473 files, more files were not shown because too many files have changed in this diff. Show more