mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 20:31:14 +00:00
Merge branch 'main' into feat/texdisp-tooltip-media
This commit is contained in:
+28
-6
@@ -99,6 +99,7 @@
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/GCode.hpp"
|
||||
#include "libslic3r/MeshRepair.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/ModelArrange.hpp"
|
||||
#include "libslic3r/Platform.hpp"
|
||||
@@ -206,7 +207,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_NO_SUITABLE_OBJECTS, "One of the plate is empty or has no object fully inside it. Please check that the 3mf contains no empty plate in Orca Slicer before uploading."},
|
||||
{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."},
|
||||
{CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."},
|
||||
@@ -1463,7 +1464,7 @@ int CLI::run(int argc, char **argv)
|
||||
//int arrange_option;
|
||||
int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1;
|
||||
bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false;
|
||||
bool allow_rotations = true, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
|
||||
bool allow_rotations = false, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
|
||||
Semver file_version;
|
||||
std::map<size_t, bool> orients_requirement;
|
||||
std::vector<Preset*> project_presets;
|
||||
@@ -5130,9 +5131,19 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "repair") {
|
||||
// Models are repaired by default.
|
||||
//for (auto &model : m_models)
|
||||
// model.repair();
|
||||
if (m_config.opt_bool(opt_key))
|
||||
for (Model &model : m_models)
|
||||
for (ModelObject *object : model.objects) {
|
||||
for (ModelVolume *volume : object->volumes) {
|
||||
MeshRepairStats stats;
|
||||
std::string error;
|
||||
if (!volume->repair(true, &stats, &error))
|
||||
BOOST_LOG_TRIVIAL(warning) << "repair of " << object->name << " / " << volume->name << " failed: " << error;
|
||||
else if (stats.modified)
|
||||
BOOST_LOG_TRIVIAL(info) << "repaired " << object->name << " / " << volume->name;
|
||||
}
|
||||
object->invalidate_bounding_box();
|
||||
}
|
||||
} else {
|
||||
boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info);
|
||||
@@ -5142,6 +5153,7 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n";
|
||||
bool oriented_or_arranged = false;
|
||||
bool arranged_without_rotations = false;
|
||||
//BBS: add orient and arrange logic here
|
||||
for (auto& model : m_models)
|
||||
{
|
||||
@@ -5409,6 +5421,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1);
|
||||
arrangement::arrange(selected, unselected, beds, arrange_cfg);
|
||||
//arrangement::arrange(unprintable, {}, beds, arrange_cfg);
|
||||
arranged_without_rotations |= !arrange_cfg.allow_rotations;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1);
|
||||
|
||||
//step-4: postprocess the bed index and result
|
||||
@@ -5881,6 +5894,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count;
|
||||
arrangement::arrange(selected, unselected, beds, arrange_cfg);
|
||||
arrangement::arrange(unprintable, {}, beds, arrange_cfg);
|
||||
arranged_without_rotations |= !arrange_cfg.allow_rotations;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count;
|
||||
|
||||
//Step-4:postprocess by partplate list&&apply the result
|
||||
@@ -6425,7 +6439,15 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
if (count == 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, cli_errors[CLI_NO_SUITABLE_OBJECTS], sliced_info);
|
||||
std::string no_objects_reason = cli_errors[CLI_NO_SUITABLE_OBJECTS];
|
||||
if (arranged_without_rotations) {
|
||||
//ORCA: result.json is only written on Linux, so also print the reason to stderr.
|
||||
no_objects_reason = "No object is fully inside the print volume. Arrange ran without rotations."
|
||||
" If an object only fits the bed when rotated, pass --allow-rotations."
|
||||
" Otherwise the plate is empty or an object is larger than the printable area or height.";
|
||||
boost::nowide::cerr << "plate " << index+1 << ": " << no_objects_reason << std::endl;
|
||||
}
|
||||
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, no_objects_reason, sliced_info);
|
||||
flush_and_exit(CLI_NO_SUITABLE_OBJECTS);
|
||||
}
|
||||
else if ((plate_to_slice != 0) || pre_check) {
|
||||
|
||||
@@ -1374,6 +1374,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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -300,12 +300,10 @@ template<class PConf>
|
||||
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -625,6 +625,7 @@ add_library(libslic3r_cgal STATIC
|
||||
CutSurface.hpp CutSurface.cpp
|
||||
IntersectionPoints.hpp IntersectionPoints.cpp
|
||||
MeshBoolean.hpp MeshBoolean.cpp
|
||||
MeshRepair.hpp MeshRepair.cpp
|
||||
TryCatchSignal.hpp TryCatchSignal.cpp
|
||||
Triangulation.hpp Triangulation.cpp
|
||||
)
|
||||
@@ -652,7 +653,9 @@ if (IS_CLANG_CL)
|
||||
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
|
||||
endif ()
|
||||
|
||||
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs)
|
||||
# Lets CGAL algorithms given CGAL::Parallel_if_available_tag run on TBB.
|
||||
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_LINKED_WITH_TBB)
|
||||
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs TBB::tbb)
|
||||
|
||||
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
|
||||
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -236,19 +237,29 @@ static void getNamedSolids(const TopLoc_Location& location,
|
||||
int i = 0;
|
||||
switch (shape_type) {
|
||||
case TopAbs_COMPOUND:
|
||||
if (!isSplitCompound) {
|
||||
namedSolids.emplace_back(TopoDS::Compound(transform.Shape()), fullName);
|
||||
break;
|
||||
}
|
||||
case TopAbs_COMPSOLID:
|
||||
if (!isSplitCompound) {
|
||||
namedSolids.emplace_back(TopoDS::CompSolid(transform.Shape()), fullName);
|
||||
namedSolids.emplace_back(transform.Shape(), fullName);
|
||||
} else {
|
||||
for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) {
|
||||
i++;
|
||||
const TopoDS_Shape& currentShape = explorer.Current();
|
||||
namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i));
|
||||
}
|
||||
// Surface models have no solids: each shell outside a solid is a part, and loose faces stay together.
|
||||
i = 0;
|
||||
for (explorer.Init(transform.Shape(), TopAbs_SHELL, TopAbs_SOLID); explorer.More(); explorer.Next())
|
||||
namedSolids.emplace_back(explorer.Current(), fullName + "-SHELL-" + std::to_string(++i));
|
||||
TopoDS_Compound loose_faces;
|
||||
TopoDS_Builder builder;
|
||||
builder.MakeCompound(loose_faces);
|
||||
bool has_loose_faces = false;
|
||||
for (explorer.Init(transform.Shape(), TopAbs_FACE, TopAbs_SHELL); explorer.More(); explorer.Next()) {
|
||||
builder.Add(loose_faces, explorer.Current());
|
||||
has_loose_faces = true;
|
||||
}
|
||||
if (has_loose_faces)
|
||||
namedSolids.emplace_back(loose_faces, fullName);
|
||||
}
|
||||
break;
|
||||
case TopAbs_SOLID:
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "libslic3r/BrimEarsPoint.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include "bbs_3mf.hpp"
|
||||
@@ -50,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>
|
||||
@@ -354,6 +356,13 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
|
||||
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
|
||||
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
|
||||
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
|
||||
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
|
||||
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
|
||||
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
|
||||
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
|
||||
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
|
||||
// BBS
|
||||
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
|
||||
|
||||
@@ -847,6 +856,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
std::vector<std::string> custom_seam;
|
||||
std::vector<std::string> mmu_segmentation;
|
||||
std::vector<std::string> fuzzy_skin;
|
||||
// One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
|
||||
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
|
||||
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
|
||||
// BBS
|
||||
std::vector<std::string> face_properties;
|
||||
|
||||
@@ -867,6 +879,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
custom_seam.clear();
|
||||
mmu_segmentation.clear();
|
||||
fuzzy_skin.clear();
|
||||
for (std::vector<std::string> &slot : texture_displacement)
|
||||
slot.clear();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -974,6 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
|
||||
MetadataList metadata;
|
||||
VolumeMetadataList volumes;
|
||||
bool auto_drop { true };
|
||||
};
|
||||
|
||||
struct CutObjectInfo
|
||||
@@ -1200,6 +1215,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
/*IdToSlaSupportPointsMap m_sla_support_points;
|
||||
IdToSlaDrainHolesMap m_sla_drain_holes;*/
|
||||
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
|
||||
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
|
||||
// are built only after the whole archive has been walked, so by the time a volume names its JSON
|
||||
// every file it could refer to is already in here, whatever order the archive happened to be in.
|
||||
std::map<std::string, std::string> m_texture_displacement_files;
|
||||
void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
|
||||
std::string m_curr_metadata_name;
|
||||
std::string m_curr_characters;
|
||||
std::string m_name;
|
||||
@@ -1349,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);
|
||||
|
||||
@@ -2073,6 +2093,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
else if (_is_svg_shape_file(name)) {
|
||||
_extract_embossed_svg_shape_file(name, archive, stat);
|
||||
}
|
||||
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
|
||||
std::string contents(stat.m_uncomp_size, '\0');
|
||||
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
|
||||
m_texture_displacement_files.emplace(name, std::move(contents));
|
||||
else
|
||||
add_error("Error while reading texture displacement data");
|
||||
}
|
||||
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
|
||||
m_parsing_slice_info = true;
|
||||
//extract slice info from archive
|
||||
@@ -2234,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")
|
||||
@@ -3310,6 +3339,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
}
|
||||
}*/
|
||||
|
||||
// Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
|
||||
// being built, which is after the whole archive has been walked - so every file it can refer to is
|
||||
// already extracted, and no deferral is needed.
|
||||
//
|
||||
// A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
|
||||
// loaded, which is the same state as a project saved by a build without the feature: recoverable by
|
||||
// picking the texture again rather than a hard failure.
|
||||
void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
|
||||
{
|
||||
const auto json = m_texture_displacement_files.find(json_path);
|
||||
if (json == m_texture_displacement_files.end()) {
|
||||
add_error("Missing texture displacement data: " + json_path);
|
||||
return;
|
||||
}
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementOptions options;
|
||||
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
|
||||
add_error("Malformed texture displacement data: " + json_path);
|
||||
return;
|
||||
}
|
||||
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
|
||||
// the project must never read the file it names: that would let whoever made the project pick any
|
||||
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
|
||||
// `path` stays a label of where the texture came from; a layer the archive carries no image for
|
||||
// loads without one, its settings intact, until the user picks a texture again.
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
if (layer.path_in_3mf.empty())
|
||||
continue;
|
||||
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
|
||||
if (image == m_texture_displacement_files.end()) {
|
||||
add_error("Missing texture displacement image: " + layer.path_in_3mf);
|
||||
continue;
|
||||
}
|
||||
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(), image->second.end());
|
||||
}
|
||||
volume.texture_displacement_layers = std::move(layers);
|
||||
volume.texture_displacement_options = options;
|
||||
}
|
||||
|
||||
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
|
||||
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
|
||||
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
|
||||
@@ -3966,6 +4034,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
||||
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
|
||||
mask.resize(m_curr_object->geometry.triangles.size());
|
||||
mask.back() = paint;
|
||||
}
|
||||
// BBS
|
||||
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -4047,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()
|
||||
@@ -4277,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;
|
||||
|
||||
@@ -4314,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);
|
||||
|
||||
@@ -4347,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);
|
||||
}
|
||||
@@ -4418,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;
|
||||
}
|
||||
@@ -5317,6 +5389,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->mmu_segmentation_facets.touch();
|
||||
volume->fuzzy_skin_facets.shrink_to_fit();
|
||||
volume->fuzzy_skin_facets.touch();
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<std::string> &mask = sub_object->geometry.texture_displacement[slot];
|
||||
if (mask.empty())
|
||||
continue; // no triangle painted on this slot
|
||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
||||
facets.reserve(triangles_count);
|
||||
for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
|
||||
if (!mask[i].empty())
|
||||
facets.set_triangle_from_string(i, mask[i]);
|
||||
facets.shrink_to_fit();
|
||||
facets.touch();
|
||||
}
|
||||
}
|
||||
|
||||
volume->set_type(volume_data->part_type);
|
||||
@@ -5332,6 +5416,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
for (const Metadata& metadata : volume_data->metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5496,12 +5582,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
volume->seam_facets.shrink_to_fit();
|
||||
volume->mmu_segmentation_facets.shrink_to_fit();
|
||||
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
|
||||
const std::vector<std::string> &mask = geometry.texture_displacement[slot];
|
||||
if (mask.empty())
|
||||
continue; // written by a build without the feature
|
||||
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
|
||||
facets.reserve(triangles_count);
|
||||
for (size_t i = 0; i < triangles_count; ++i) {
|
||||
const size_t index = volume_data.first_triangle_id + i;
|
||||
if (index < mask.size() && !mask[index].empty())
|
||||
facets.set_triangle_from_string(i, mask[index]);
|
||||
}
|
||||
facets.shrink_to_fit();
|
||||
facets.touch();
|
||||
}
|
||||
volume->set_type(volume_data.part_type);
|
||||
|
||||
// apply the remaining volume's metadata
|
||||
for (const Metadata& metadata : volume_data.metadata) {
|
||||
if (metadata.key == NAME_KEY)
|
||||
volume->name = metadata.value;
|
||||
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
|
||||
_apply_texture_displacement(*volume, metadata.value);
|
||||
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
|
||||
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
|
||||
//for old format
|
||||
@@ -5805,6 +5907,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
|
||||
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
|
||||
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
|
||||
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
|
||||
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
|
||||
mask.resize(current_object->geometry.triangles.size());
|
||||
mask.back() = paint;
|
||||
}
|
||||
// BBS
|
||||
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
|
||||
}
|
||||
@@ -6058,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)
|
||||
{
|
||||
}
|
||||
};
|
||||
@@ -6156,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;
|
||||
@@ -6366,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);
|
||||
@@ -6414,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6460,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))){
|
||||
@@ -6754,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;
|
||||
}
|
||||
@@ -6808,6 +6929,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
|
||||
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
|
||||
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
|
||||
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
|
||||
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
|
||||
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
|
||||
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
|
||||
stream << "</Types>";
|
||||
|
||||
@@ -6926,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";
|
||||
@@ -6937,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)
|
||||
@@ -6945,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)
|
||||
@@ -6970,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()) {
|
||||
@@ -7232,10 +7363,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (iter != m_shared_meshes.end())
|
||||
{
|
||||
const ModelVolume* shared_volume = iter->second.second;
|
||||
bool same_texture_paint = true;
|
||||
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
|
||||
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
|
||||
if ((shared_volume->supported_facets.equals(volume->supported_facets))
|
||||
&& (shared_volume->seam_facets.equals(volume->seam_facets))
|
||||
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
|
||||
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
|
||||
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
|
||||
&& same_texture_paint)
|
||||
{
|
||||
auto data = iter->second.first;
|
||||
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
|
||||
@@ -7277,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++;
|
||||
}
|
||||
|
||||
@@ -7651,6 +7786,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
|
||||
// not know, so a project written here still opens in a build without the feature - it just
|
||||
// loses the paint, which is also all it could have done with it.
|
||||
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
|
||||
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
|
||||
if (texture_paint.empty())
|
||||
continue;
|
||||
output_buffer += " ";
|
||||
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
|
||||
output_buffer += "=\"";
|
||||
output_buffer += texture_paint;
|
||||
output_buffer += "\"";
|
||||
}
|
||||
|
||||
// BBS
|
||||
if (i < its.properties.size()) {
|
||||
std::string prop_str = its.properties[i].to_string();
|
||||
@@ -7713,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";
|
||||
@@ -8100,7 +8248,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return file_path;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
|
||||
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
|
||||
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
|
||||
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
|
||||
// every reader has to parse just to list the objects.
|
||||
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
|
||||
const std::string &id)
|
||||
{
|
||||
if (volume.texture_displacement_layers.empty())
|
||||
return;
|
||||
|
||||
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
|
||||
// being exported is const (and may be saved again, elsewhere, with different paths).
|
||||
std::vector<TextureDisplacementLayer> layers = volume.texture_displacement_layers;
|
||||
for (TextureDisplacementLayer &layer : layers) {
|
||||
layer.path_in_3mf.clear();
|
||||
if (!layer.image_data || layer.image_data->empty())
|
||||
continue;
|
||||
// Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
|
||||
// greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
|
||||
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
|
||||
// No deflate: a PNG is already compressed, so a second pass only costs time.
|
||||
if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
|
||||
MZ_NO_COMPRESSION))
|
||||
layer.path_in_3mf = path;
|
||||
}
|
||||
|
||||
const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
|
||||
const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
|
||||
if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
|
||||
return;
|
||||
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
|
||||
<< VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
|
||||
{
|
||||
std::stringstream stream;
|
||||
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
|
||||
@@ -8116,7 +8299,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
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())
|
||||
@@ -8211,6 +8395,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
|
||||
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
|
||||
}
|
||||
|
||||
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
|
||||
|
||||
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
|
||||
tc.has_value())
|
||||
@@ -9591,10 +9777,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>";
|
||||
@@ -9602,14 +9791,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 (...) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+28
-11
@@ -1033,6 +1033,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return buf;
|
||||
}
|
||||
|
||||
// FanMover consumes these markers (and drops them) to avoid splitting a G1 inside custom gcode.
|
||||
// Only written when FanMover runs, the same gate as its construction in GCode::process_layers.
|
||||
static std::string wrap_custom_gcode_for_fan_mover(const FullPrintConfig &config, const std::string &gcode)
|
||||
{
|
||||
if (config.fan_speedup_time.value == 0 && config.fan_kickstart.value <= 0)
|
||||
return gcode;
|
||||
return ";_FAN_MOVER_SKIP_START\n" + gcode + ";_FAN_MOVER_SKIP_END\n";
|
||||
}
|
||||
|
||||
std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const
|
||||
{
|
||||
if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
|
||||
@@ -1384,6 +1393,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config);
|
||||
|
||||
check_add_eol(toolchange_gcode_str);
|
||||
// Type1 wipe tower path (every BBL printer): bracket the same way set_extruder() does,
|
||||
// so FanMover doesn't split a G1 inside this custom gcode either.
|
||||
toolchange_gcode_str = wrap_custom_gcode_for_fan_mover(gcodegen.config(), toolchange_gcode_str);
|
||||
|
||||
//BBS
|
||||
{
|
||||
@@ -1967,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.
|
||||
@@ -2006,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);
|
||||
@@ -2032,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;
|
||||
@@ -2780,11 +2792,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;
|
||||
}
|
||||
@@ -2806,7 +2820,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.
|
||||
@@ -2937,8 +2952,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;
|
||||
@@ -4149,7 +4165,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
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);
|
||||
}
|
||||
@@ -4250,7 +4266,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get())));
|
||||
config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get())));
|
||||
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
|
||||
std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.values.at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config);
|
||||
std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.get_at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config);
|
||||
file.writeln(filament_start_gcode);
|
||||
// Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where
|
||||
// H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their
|
||||
@@ -10895,7 +10911,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
|
||||
toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config);
|
||||
check_add_eol(toolchange_gcode_parsed);
|
||||
gcode += toolchange_gcode_parsed;
|
||||
// toolchange_gcode_parsed is still read below (get_last_z_from_gcode, custom_gcode_changes_tool).
|
||||
gcode += wrap_custom_gcode_for_fan_mover(m_config, toolchange_gcode_parsed);
|
||||
|
||||
//BBS
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -445,6 +445,11 @@ void FanMover::_process_gcode_line(GCodeReader& reader, const GCodeReader::GCode
|
||||
std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6);
|
||||
current_role = ExtrusionEntity::string_to_role(extrusion_string);
|
||||
}
|
||||
if (line.raw().rfind(";_FAN_MOVER_SKIP_", 0) == 0) {
|
||||
// Internal markers: read here, then dropped from the output.
|
||||
m_is_custom_gcode = line.raw().rfind(";_FAN_MOVER_SKIP_START", 0) == 0;
|
||||
time = -1;
|
||||
}
|
||||
if (line.raw().size() > 16) {
|
||||
if (line.raw().rfind("; custom gcode", 0) != std::string::npos) {
|
||||
if (line.raw().rfind("; custom gcode end", 0) != std::string::npos)
|
||||
|
||||
@@ -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
|
||||
@@ -2602,12 +2605,13 @@ void GCodeProcessorResult::reset() {
|
||||
//BBS: add mutex for protection of gcode result
|
||||
lock();
|
||||
|
||||
moves.clear();
|
||||
// moves and lines_ends grow with the print: free them, as no reset result reuses the capacity.
|
||||
clear_and_shrink(moves);
|
||||
plate_mass = {};
|
||||
object_masses.clear();
|
||||
body_masses.clear();
|
||||
support_masses.clear();
|
||||
lines_ends.clear();
|
||||
clear_and_shrink(lines_ends);
|
||||
printable_area = Pointfs();
|
||||
//BBS: add bed exclude area
|
||||
bed_exclude_area = Pointfs();
|
||||
@@ -3598,6 +3602,10 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
|
||||
if (has_scarf_joint_seam != nullptr)
|
||||
m_detect_layer_based_on_tag = m_detect_layer_based_on_tag || has_scarf_joint_seam->value;
|
||||
|
||||
const ConfigOptionBool* manual_filament_change = config.option<ConfigOptionBool>("manual_filament_change");
|
||||
if (manual_filament_change != nullptr)
|
||||
m_manual_filament_change = manual_filament_change->value;
|
||||
|
||||
const ConfigOptionEnumGeneric *bed_type = config.option<ConfigOptionEnumGeneric>("curr_bed_type");
|
||||
if (bed_type != nullptr)
|
||||
m_result.bed_type = (BedType)bed_type->value;
|
||||
@@ -3722,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;
|
||||
}
|
||||
@@ -3743,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;
|
||||
}
|
||||
|
||||
@@ -3936,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);
|
||||
}
|
||||
@@ -6123,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1228,7 +1228,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
|
||||
// m_result.nozzle_group_result, which is handed over only after the stream for the
|
||||
// pre-heat injector's second pass and gates the richer change-time model.
|
||||
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
|
||||
bool m_manual_filament_change;
|
||||
bool m_manual_filament_change{ false };
|
||||
|
||||
//BBS: x, y offset for gcode generated
|
||||
double m_x_offset{ 0 };
|
||||
@@ -1280,6 +1280,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
#include "JumpPointSearch.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "libslic3r/AStar.hpp"
|
||||
#include "libslic3r/KDTreeIndirect.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <oneapi/tbb/scalable_allocator.h>
|
||||
|
||||
//#define DEBUG_FILES
|
||||
#ifdef DEBUG_FILES
|
||||
#include "libslic3r/SVG.hpp"
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// execute fn for each pixel on the line. If fn returns false, terminate the iteration
|
||||
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||
{
|
||||
coord_t dx = abs(x1 - x0);
|
||||
coord_t dy = abs(y1 - y0);
|
||||
coord_t x = x0;
|
||||
coord_t y = y0;
|
||||
coord_t n = 1 + dx + dy;
|
||||
coord_t x_inc = (x1 > x0) ? 1 : -1;
|
||||
coord_t y_inc = (y1 > y0) ? 1 : -1;
|
||||
coord_t error = dx - dy;
|
||||
dx *= 2;
|
||||
dy *= 2;
|
||||
|
||||
for (; n > 0; --n) {
|
||||
if (!fn(x, y)) return;
|
||||
|
||||
if (error > 0) {
|
||||
x += x_inc;
|
||||
error -= dy;
|
||||
} else {
|
||||
y += y_inc;
|
||||
error += dx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// will draw the line twice, second time with and offset of 1 in the direction of normal
|
||||
// may call the fn on the same coordiantes multiple times!
|
||||
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
|
||||
{
|
||||
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
|
||||
normal.x() = ceil(normal.x());
|
||||
normal.y() = ceil(normal.y());
|
||||
Point start_offset = Point(x0, y0) + (normal).cast<coord_t>();
|
||||
Point end_offset = Point(x1, y1) + (normal).cast<coord_t>();
|
||||
|
||||
dda(x0, y0, x1, y1, fn);
|
||||
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
|
||||
}
|
||||
|
||||
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
|
||||
{
|
||||
public:
|
||||
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
|
||||
struct Node
|
||||
{
|
||||
CellPositionType position;
|
||||
CellPositionType incoming_dir;
|
||||
};
|
||||
|
||||
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
|
||||
|
||||
private:
|
||||
CellPositionType target;
|
||||
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
|
||||
|
||||
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
|
||||
{
|
||||
CellPositionType next = start + forward_dir;
|
||||
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
|
||||
|
||||
if (is_passable(next)) {
|
||||
return next;
|
||||
} else {
|
||||
return start;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
|
||||
{
|
||||
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||
// diagonal
|
||||
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||
|
||||
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
|
||||
|
||||
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
|
||||
|
||||
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
|
||||
|
||||
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
|
||||
|
||||
return false;
|
||||
} else { // horizontal or vertical
|
||||
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||
|
||||
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
|
||||
|
||||
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
|
||||
{
|
||||
const CellPositionType & pos = from.position;
|
||||
const CellPositionType & forward_dir = from.incoming_dir;
|
||||
std::vector<CellPositionType> dirs_to_check{};
|
||||
|
||||
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
|
||||
dirs_to_check = all_directions;
|
||||
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
|
||||
// diagonal
|
||||
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
|
||||
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
|
||||
|
||||
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
|
||||
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
|
||||
}
|
||||
|
||||
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
|
||||
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
|
||||
}
|
||||
|
||||
dirs_to_check.push_back(horizontal_check_dir);
|
||||
dirs_to_check.push_back(vertical_check_dir);
|
||||
dirs_to_check.push_back(forward_dir);
|
||||
|
||||
} else { // horizontal or vertical
|
||||
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
|
||||
|
||||
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
|
||||
|
||||
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
|
||||
dirs_to_check.push_back(forward_dir);
|
||||
}
|
||||
|
||||
for (const CellPositionType &dir : dirs_to_check) {
|
||||
CellPositionType jp = find_jump_point(pos, dir);
|
||||
if (jp != pos) fn(Node{jp, dir});
|
||||
}
|
||||
}
|
||||
|
||||
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
|
||||
|
||||
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
|
||||
|
||||
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
|
||||
|
||||
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
|
||||
};
|
||||
|
||||
void JPSPathFinder::clear()
|
||||
{
|
||||
inpassable.clear();
|
||||
max_search_box.max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
|
||||
max_search_box.min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
|
||||
add_obstacles(bed_shape);
|
||||
}
|
||||
|
||||
void JPSPathFinder::add_obstacles(const Lines &obstacles)
|
||||
{
|
||||
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||
max_search_box.max.x() = std::max(max_search_box.max.x(), x);
|
||||
max_search_box.max.y() = std::max(max_search_box.max.y(), y);
|
||||
max_search_box.min.x() = std::min(max_search_box.min.x(), x);
|
||||
max_search_box.min.y() = std::min(max_search_box.min.y(), y);
|
||||
inpassable.insert(Pixel{x, y});
|
||||
return true;
|
||||
};
|
||||
|
||||
for (const Line &l : obstacles) {
|
||||
Pixel start = pixelize(l.a);
|
||||
Pixel end = pixelize(l.b);
|
||||
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
|
||||
}
|
||||
}
|
||||
|
||||
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
|
||||
{
|
||||
Pixel start = pixelize(p0);
|
||||
Pixel end = pixelize(p1);
|
||||
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
|
||||
|
||||
if (inpassable.find(start) != inpassable.end()) {
|
||||
dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
|
||||
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
|
||||
start = Pixel(x, y);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (inpassable.find(end) != inpassable.end()) {
|
||||
dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
|
||||
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
|
||||
end = Pixel(x, y);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
BoundingBox search_box = max_search_box;
|
||||
search_box.max -= Pixel(1, 1);
|
||||
search_box.min += Pixel(1, 1);
|
||||
|
||||
BoundingBox bounding_square(Points{start, end});
|
||||
bounding_square.max += Pixel(5, 5);
|
||||
bounding_square.min -= Pixel(5, 5);
|
||||
coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
|
||||
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
|
||||
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
|
||||
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
|
||||
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
|
||||
|
||||
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
|
||||
search_box.max = search_box.max.cwiseMin(bounding_square.max);
|
||||
search_box.min = search_box.min.cwiseMax(bounding_square.min);
|
||||
|
||||
auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
|
||||
|
||||
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
|
||||
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
|
||||
|
||||
std::unordered_map<size_t, QNode> astar_cache{};
|
||||
std::vector<Pixel, PointsAllocator<Pixel>> out_path;
|
||||
std::vector<decltype(tracer)::Node> out_nodes;
|
||||
|
||||
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
|
||||
// path not found - just reconstruct the best path from astar cache.
|
||||
// Note that astar_cache is NOT empty - at least the starting point should always be there
|
||||
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
|
||||
std::vector<size_t> keys;
|
||||
keys.reserve(astar_cache.size());
|
||||
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
|
||||
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
|
||||
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
|
||||
|
||||
out_path.push_back(end);
|
||||
while (closest_qnode != astar::Unassigned) {
|
||||
out_path.push_back(astar_cache[closest_qnode].node.position);
|
||||
closest_qnode = astar_cache[closest_qnode].parent;
|
||||
}
|
||||
} else {
|
||||
for (const auto &node : out_nodes) { out_path.push_back(node.position); }
|
||||
out_path.push_back(start);
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FILES
|
||||
auto scaled_points = [](const Points &ps) {
|
||||
Points r;
|
||||
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
|
||||
return r;
|
||||
};
|
||||
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
|
||||
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
|
||||
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
|
||||
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
|
||||
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
|
||||
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
|
||||
svg.draw(scaled_point(end), "purple", scale_(0.4));
|
||||
svg.draw(scaled_point(start), "green", scale_(0.4));
|
||||
#endif
|
||||
|
||||
std::vector<Pixel, PointsAllocator<Pixel>> tmp_path;
|
||||
tmp_path.reserve(out_path.size());
|
||||
// Some path found, reverse and remove points that do not change direction
|
||||
std::reverse(out_path.begin(), out_path.end());
|
||||
{
|
||||
tmp_path.push_back(out_path.front()); // first point
|
||||
for (size_t i = 1; i < out_path.size() - 1; i++) {
|
||||
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) { tmp_path.push_back(out_path[i]); }
|
||||
}
|
||||
tmp_path.push_back(out_path.back()); // last_point
|
||||
out_path = tmp_path;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FILES
|
||||
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
|
||||
#endif
|
||||
|
||||
tmp_path.clear();
|
||||
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
|
||||
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
|
||||
// insert the Px+k point to result and continue search from Px+k
|
||||
{
|
||||
tmp_path.push_back(out_path.front()); // first point
|
||||
size_t index_of_last_stored_point = 0;
|
||||
for (size_t i = 1; i < out_path.size(); i++) {
|
||||
if (i - index_of_last_stored_point < 2) continue;
|
||||
bool passable = true;
|
||||
auto store_obstacle = [&](coord_t x, coord_t y) {
|
||||
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
|
||||
passable = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
|
||||
if (!passable) {
|
||||
tmp_path.push_back(out_path[i - 1]);
|
||||
index_of_last_stored_point = i - 1;
|
||||
}
|
||||
}
|
||||
tmp_path.push_back(out_path.back()); // last_point
|
||||
out_path = tmp_path;
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FILES
|
||||
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
|
||||
svg.Close();
|
||||
#endif
|
||||
|
||||
// before returing the path, transform it from pixels back to points.
|
||||
// Also replace the first and last pixel by input points so that result path patches input params exactly.
|
||||
for (Pixel &p : out_path) { p = unpixelize(p); }
|
||||
out_path.front() = p0;
|
||||
out_path.back() = p1;
|
||||
|
||||
return Polyline(out_path);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,39 +0,0 @@
|
||||
#pragma once
|
||||
#include "Line.hpp"
|
||||
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class JPSPathFinder
|
||||
{
|
||||
using Pixel = Point;
|
||||
std::unordered_set<Pixel, PointHash> inpassable;
|
||||
coordf_t print_z;
|
||||
BoundingBox max_search_box;
|
||||
Lines bed_shape;
|
||||
|
||||
const coord_t resolution = scaled(1.5);
|
||||
Pixel pixelize(const Point &p) { return p / resolution; }
|
||||
Point unpixelize(const Pixel &p) { return p * resolution; }
|
||||
|
||||
public:
|
||||
JPSPathFinder() = default;
|
||||
void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
|
||||
void clear();
|
||||
void add_obstacles(const Lines &obstacles);
|
||||
Polyline find_path(const Point &start, const Point &end);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
|
||||
@@ -29,8 +29,6 @@
|
||||
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
|
||||
#include <CGAL/Polygon_mesh_processing/stitch_borders.h>
|
||||
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair_degeneracies.h>
|
||||
#include <CGAL/Polygon_mesh_processing/manifoldness.h>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <algorithm>
|
||||
@@ -50,7 +48,6 @@
|
||||
#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair.h>
|
||||
#include <CGAL/Polygon_mesh_processing/remesh.h>
|
||||
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
|
||||
#include <CGAL/Polygon_mesh_processing/orientation.h>
|
||||
// BBS: for segment
|
||||
#include <CGAL/mesh_segmentation.h>
|
||||
@@ -658,91 +655,6 @@ bool empty(const CGALMesh &mesh)
|
||||
return mesh.m.is_empty();
|
||||
}
|
||||
|
||||
bool repair(TriangleMesh& mesh, RepairedMeshErrors* repaired_errors, std::string* error)
|
||||
{
|
||||
namespace PMP = CGAL::Polygon_mesh_processing;
|
||||
|
||||
if (mesh.empty())
|
||||
return true;
|
||||
|
||||
try {
|
||||
// 1) Convert to polygon soup
|
||||
std::vector<_EpicMesh::Point> points;
|
||||
std::vector<std::vector<std::size_t>> polygons;
|
||||
|
||||
points.reserve(mesh.its.vertices.size());
|
||||
polygons.reserve(mesh.its.indices.size());
|
||||
|
||||
for (const auto& v : mesh.its.vertices)
|
||||
points.emplace_back(v.x(), v.y(), v.z());
|
||||
|
||||
for (const auto& f : mesh.its.indices)
|
||||
polygons.push_back({size_t(f[0]), size_t(f[1]), size_t(f[2])});
|
||||
|
||||
// 2) Aggressive soup cleanup
|
||||
PMP::repair_polygon_soup(points, polygons);
|
||||
|
||||
// 3) Convert soup → mesh
|
||||
_EpicMesh cgal_mesh;
|
||||
PMP::polygon_soup_to_polygon_mesh(points, polygons, cgal_mesh);
|
||||
|
||||
// 4) Remove degenerate geometry
|
||||
PMP::remove_degenerate_faces(cgal_mesh);
|
||||
PMP::remove_isolated_vertices(cgal_mesh);
|
||||
|
||||
// 5) Fix remaining non-manifold vertices
|
||||
PMP::duplicate_non_manifold_vertices(cgal_mesh);
|
||||
|
||||
// 6) Boolean union (keeps only outer shell)
|
||||
_EpicMesh tmp;
|
||||
if (PMP::corefine_and_compute_union(cgal_mesh, cgal_mesh, tmp)) {
|
||||
cgal_mesh = std::move(tmp);
|
||||
}
|
||||
// If it fails, continue anyway with previous mesh
|
||||
|
||||
// 7) Fill holes
|
||||
if (!CGAL::is_closed(cgal_mesh)) {
|
||||
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
|
||||
|
||||
std::vector<halfedge_descriptor> borders;
|
||||
CGAL::extract_boundary_cycles(cgal_mesh, std::back_inserter(borders));
|
||||
|
||||
for (halfedge_descriptor h : borders) {
|
||||
PMP::triangulate_and_refine_hole(cgal_mesh, h);
|
||||
}
|
||||
}
|
||||
|
||||
// 8) Final validity check
|
||||
if (!CGAL::is_closed(cgal_mesh)) {
|
||||
if (error)
|
||||
*error = "Repair failed: mesh still open after hole filling.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 9) Ensure outward orientation
|
||||
if (!PMP::does_bound_a_volume(cgal_mesh))
|
||||
PMP::orient_to_bound_a_volume(cgal_mesh);
|
||||
|
||||
// 10) Convert back
|
||||
indexed_triangle_set its = cgal_to_indexed_triangle_set(cgal_mesh);
|
||||
|
||||
RepairedMeshErrors errs{};
|
||||
errs.facets_removed = 0;
|
||||
errs.edges_fixed = 0;
|
||||
|
||||
mesh = TriangleMesh(std::move(its), errs);
|
||||
|
||||
if (repaired_errors)
|
||||
*repaired_errors = errs;
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
if (error)
|
||||
*error = e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
CGALMeshPtr clone(const CGALMesh &m)
|
||||
{
|
||||
return CGALMeshPtr{new CGALMesh{m}};
|
||||
|
||||
@@ -77,9 +77,6 @@ TriangleMesh merge(std::vector<TriangleMesh> meshes);
|
||||
bool does_bound_a_volume(const CGALMesh &mesh);
|
||||
bool empty(const CGALMesh &mesh);
|
||||
|
||||
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
|
||||
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
|
||||
|
||||
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
|
||||
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
|
||||
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh;
|
||||
|
||||
struct MeshRepairStats
|
||||
{
|
||||
std::size_t welded_vertices = 0;
|
||||
std::size_t removed_triangles = 0;
|
||||
std::size_t reversed_shells = 0;
|
||||
std::size_t filled_holes = 0;
|
||||
std::size_t removed_shells = 0;
|
||||
std::size_t self_intersections = 0;
|
||||
// False when the input already was closed, consistently oriented and free of self-intersections.
|
||||
bool modified = false;
|
||||
};
|
||||
|
||||
// Returns false, leaving the mesh untouched, if no solid can be built. progress gets the percentage done and may
|
||||
// throw to cancel. See docs/HLSD/mesh-repair.md.
|
||||
bool repair_mesh(TriangleMesh &mesh, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
|
||||
const std::function<void(int)> &progress = {});
|
||||
|
||||
} // namespace Slic3r
|
||||
+69
-31
@@ -29,6 +29,7 @@
|
||||
#include "TriangleMeshSlicer.hpp"
|
||||
#include "TriangleSelector.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
#include "MeshRepair.hpp"
|
||||
|
||||
#include "Format/AMF.hpp"
|
||||
#include "Format/svg.hpp"
|
||||
@@ -353,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.
|
||||
@@ -453,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);
|
||||
@@ -1261,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);
|
||||
@@ -1301,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);
|
||||
|
||||
@@ -1870,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) {
|
||||
@@ -1888,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)
|
||||
@@ -2133,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
|
||||
@@ -2946,6 +2968,18 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
|
||||
if (text_configuration.has_value())
|
||||
text_configuration.reset();
|
||||
|
||||
// Parts without a convex hull enclose no volume; drop them here, as deleting volumes later could delete this one.
|
||||
std::vector<std::unique_ptr<ModelVolume>> parts;
|
||||
for (TriangleMesh &mesh : meshes)
|
||||
if (!mesh.empty()) {
|
||||
std::unique_ptr<ModelVolume> part(new ModelVolume(object, *this, std::move(mesh)));
|
||||
if (const std::shared_ptr<const TriangleMesh> &hull = part->get_convex_hull_shared_ptr();
|
||||
hull != nullptr && !hull->its.vertices.empty() && !hull->its.indices.empty())
|
||||
parts.emplace_back(std::move(part));
|
||||
}
|
||||
if (parts.empty())
|
||||
return 1;
|
||||
|
||||
std::optional<TriangleSelector::SavedPainting> saved_painting = remap_paint ? save_painting() :
|
||||
std::optional<TriangleSelector::SavedPainting>{};
|
||||
|
||||
@@ -2956,26 +2990,22 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
|
||||
unsigned int extruder_counter = 0;
|
||||
const Vec3d offset = this->get_offset();
|
||||
|
||||
for (TriangleMesh &mesh : meshes) {
|
||||
if (mesh.empty())
|
||||
// Repair may have removed unconnected triangles, thus emptying the mesh.
|
||||
continue;
|
||||
|
||||
for (std::unique_ptr<ModelVolume> &part : parts) {
|
||||
if (idx == 0) {
|
||||
this->set_mesh(std::move(mesh));
|
||||
this->calculate_convex_hull();
|
||||
this->set_mesh(part->m_mesh);
|
||||
m_convex_hull = part->m_convex_hull;
|
||||
this->invalidate_convex_hull_2d();
|
||||
// Assign a new unique ID, so that a new GLVolume will be generated.
|
||||
this->set_new_unique_id();
|
||||
// reset the source to disable reload from disk
|
||||
this->source = ModelVolume::Source();
|
||||
|
||||
// BBS: reset facet annotations
|
||||
this->reset_extra_facets();
|
||||
}
|
||||
else
|
||||
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh)));
|
||||
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), part.release());
|
||||
|
||||
// reset the source to disable reload from disk
|
||||
this->object->volumes[ivolume]->source = ModelVolume::Source();
|
||||
this->object->volumes[ivolume]->set_offset(Vec3d::Zero());
|
||||
this->object->volumes[ivolume]->center_geometry_after_creation();
|
||||
this->object->volumes[ivolume]->translate(offset);
|
||||
@@ -2989,21 +3019,29 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
|
||||
++ idx;
|
||||
}
|
||||
|
||||
// discard volumes for which the convex hull was not generated or is degenerate
|
||||
size_t i = 0;
|
||||
while (i < this->object->volumes.size()) {
|
||||
const std::shared_ptr<const TriangleMesh> &hull = this->object->volumes[i]->get_convex_hull_shared_ptr();
|
||||
if (hull == nullptr || hull->its.vertices.empty() || hull->its.indices.empty()) {
|
||||
this->object->delete_volume(i);
|
||||
--idx;
|
||||
--i;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
bool ModelVolume::repair(bool keep_painting, MeshRepairStats *stats_out, std::string *error, const std::function<void(int)> &progress)
|
||||
{
|
||||
TriangleMesh mesh = this->mesh();
|
||||
MeshRepairStats stats;
|
||||
const bool ok = repair_mesh(mesh, &stats, error, progress);
|
||||
if (stats_out)
|
||||
*stats_out = stats;
|
||||
if (!ok || !stats.modified)
|
||||
return ok;
|
||||
|
||||
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ? save_painting() : std::nullopt;
|
||||
this->set_mesh(std::move(mesh));
|
||||
this->calculate_convex_hull();
|
||||
this->invalidate_convex_hull_2d();
|
||||
// Assign a new unique ID, so that a new GLVolume will be generated.
|
||||
this->set_new_unique_id();
|
||||
this->restore_painting(saved_painting);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModelVolume::translate(const Vec3d& displacement)
|
||||
{
|
||||
set_offset(get_offset() + displacement);
|
||||
|
||||
+41
-15
@@ -77,6 +77,7 @@ class Preset;
|
||||
class BBLProject;
|
||||
|
||||
class KeyStore;
|
||||
struct MeshRepairStats;
|
||||
|
||||
namespace UndoRedo {
|
||||
class StackImpl;
|
||||
@@ -399,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.
|
||||
@@ -705,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,
|
||||
@@ -718,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,
|
||||
@@ -887,13 +891,13 @@ public:
|
||||
// The triangular model.
|
||||
const TriangleMesh& mesh() const { return *m_mesh.get(); }
|
||||
std::shared_ptr<const TriangleMesh> mesh_ptr() const { return m_mesh; }
|
||||
void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); }
|
||||
void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); }
|
||||
void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); }
|
||||
void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); }
|
||||
void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; }
|
||||
void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); }
|
||||
void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); }
|
||||
void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; }
|
||||
void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; }
|
||||
void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; }
|
||||
void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; }
|
||||
void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; m_is_splittable = -1; }
|
||||
void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); m_is_splittable = -1; }
|
||||
void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); m_is_splittable = -1; }
|
||||
const std::shared_ptr<const TriangleMesh>& get_mesh_shared_ptr() const { return m_mesh; }
|
||||
// Configuration parameters specific to an object model geometry or a modifier volume,
|
||||
// overriding the global Slic3r settings and the ModelObject settings.
|
||||
@@ -974,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).
|
||||
@@ -1043,6 +1067,9 @@ public:
|
||||
// Return the number of volumes created from this one.
|
||||
// This is useful to assign different materials to different volumes of an object.
|
||||
size_t split(unsigned int max_extruders, bool remap_paint);
|
||||
// Replaces the mesh by its repair_mesh() result if that changed it. False if no solid can be built.
|
||||
bool repair(bool keep_painting, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
|
||||
const std::function<void(int)> &progress = {});
|
||||
void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); }
|
||||
void translate(const Vec3d& displacement);
|
||||
void scale(const Vec3d& scaling_factors);
|
||||
@@ -1236,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());
|
||||
@@ -1334,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);
|
||||
@@ -1358,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);
|
||||
@@ -1402,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
|
||||
@@ -1532,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());
|
||||
}
|
||||
@@ -1543,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()); }
|
||||
|
||||
@@ -1557,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
@@ -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 ¶m)
|
||||
{
|
||||
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 ¶m) {
|
||||
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 {
|
||||
|
||||
+167
-13
@@ -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());
|
||||
@@ -2164,6 +2175,16 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map<std
|
||||
DynamicPrintConfig temp_config;
|
||||
std::vector<std::string> dirty_options = preset.config.diff(parent_preset->config);
|
||||
|
||||
// Send the variant layout even when it matches the parent's, as the local save does: the loader
|
||||
// sizes the per-variant values by it and would otherwise keep only the first one.
|
||||
std::string extruder_id_name, extruder_variant_name;
|
||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||
if (!extruder_id_name.empty())
|
||||
dirty_options.emplace_back(extruder_id_name);
|
||||
if (!extruder_variant_name.empty())
|
||||
dirty_options.emplace_back(extruder_variant_name);
|
||||
|
||||
for (auto option: dirty_options)
|
||||
{
|
||||
ConfigOption *opt_src = preset.config.option(option);
|
||||
@@ -2628,15 +2649,34 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
|
||||
new_config = default_preset.config;
|
||||
}
|
||||
|
||||
std::string extruder_id_name, extruder_variant_name;
|
||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||
|
||||
// Older uploads left out a variant layout equal to the parent's but still sent one value per
|
||||
// parent variant. Read those values against the parent's layout instead of cutting them to one.
|
||||
if (inherit_preset && !cloud_config.has(extruder_variant_name)) {
|
||||
const auto *parent_variants = inherit_preset->config.option<ConfigOptionStrings>(extruder_variant_name);
|
||||
const size_t variant_count = parent_variants ? parent_variants->size() : 0;
|
||||
bool values_per_variant = false;
|
||||
for (const std::string &key : cloud_config.keys()) {
|
||||
const size_t stride = key_set2->count(key) ? 2 : key_set1->count(key) ? 1 : 0;
|
||||
const auto *values = dynamic_cast<const ConfigOptionVectorBase *>(cloud_config.option(key));
|
||||
if (stride > 0 && key != extruder_id_name && values && variant_count > 1 && values->size() == variant_count * stride) {
|
||||
values_per_variant = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (values_per_variant)
|
||||
for (const std::string &key : {extruder_id_name, extruder_variant_name})
|
||||
if (!key.empty() && !cloud_config.has(key) && inherit_preset->config.has(key))
|
||||
cloud_config.set_key_value(key, inherit_preset->config.option(key)->clone());
|
||||
}
|
||||
|
||||
extend_default_config_length(cloud_config, false, {});
|
||||
|
||||
if (inherit_preset) {
|
||||
std::string extruder_id_name, extruder_variant_name;
|
||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||
|
||||
if (inherit_preset)
|
||||
new_config.update_diff_values_to_child_config(cloud_config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||
}
|
||||
else{
|
||||
new_config.apply(std::move(cloud_config));
|
||||
extend_default_config_length(new_config, true, default_preset.config);
|
||||
@@ -2728,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)
|
||||
{
|
||||
@@ -2992,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)
|
||||
@@ -4611,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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+115
-110
@@ -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 ®ion : 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),
|
||||
@@ -2991,6 +3024,15 @@ void PrintObject::clear_shared_object()
|
||||
void PrintObject::copy_layers_from_shared_object()
|
||||
{
|
||||
if (m_shared_object) {
|
||||
// Orca: Print::process() calls this once per slice for each sharing object, after clear_shared_object() has
|
||||
// emptied any earlier alias, so the vectors hold only layers this object sliced itself before it started sharing
|
||||
// (or nothing). Free them here, as clear_support_layers() / clear_layers() would; those skip deletion while
|
||||
// m_shared_object is set. The belt-only data they also free is always empty here, because belt printers never
|
||||
// share layers.
|
||||
for (SupportLayer *layer : m_support_layers)
|
||||
delete layer;
|
||||
for (Layer *layer : m_layers)
|
||||
delete layer;
|
||||
m_layers.clear();
|
||||
m_support_layers.clear();
|
||||
|
||||
@@ -3449,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);
|
||||
@@ -5398,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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5517,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();
|
||||
@@ -5631,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] =";
|
||||
|
||||
|
||||
+17
-5
@@ -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.
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
+136
-128
@@ -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);
|
||||
@@ -12866,10 +12865,11 @@ CLITransformConfigDef::CLITransformConfigDef()
|
||||
//def->cli = "orient|o";
|
||||
def->set_default_value(new ConfigOptionInt(0));
|
||||
|
||||
/*def = this->add("repair", coBool);
|
||||
def = this->add("repair", coBool);
|
||||
def->label = L("Repair");
|
||||
def->tooltip = L("Repair the model's meshes if it is non-manifold mesh.");
|
||||
def->set_default_value(new ConfigOptionBool(false));*/
|
||||
def->tooltip = L("Repair the meshes of the models as Fix Model does: close holes and cracks, orient them outward and "
|
||||
"remove self-intersections.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("rotate", coFloat);
|
||||
def->label = L("Rotate");
|
||||
@@ -12925,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");
|
||||
@@ -13085,7 +13085,7 @@ CLIMiscConfigDef::CLIMiscConfigDef()
|
||||
def = this->add("allow_rotations", coBool);
|
||||
def->label = L("Allow rotation when arranging");
|
||||
def->tooltip = L("If enabled, Arrange will allow rotation when placing objects.");
|
||||
def->set_default_value(new ConfigOptionBool(true));
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("align_to_y_axis", coBool);
|
||||
def->label = L("Align to Y axis when arranging");
|
||||
@@ -13233,12 +13233,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()
|
||||
@@ -13356,8 +13356,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()
|
||||
@@ -13396,22 +13396,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."))
|
||||
}
|
||||
|
||||
|
||||
@@ -13518,42 +13518,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -982,6 +982,9 @@ void PrintObject::generate_support_material(bool with_belt_brim)
|
||||
this->_generate_support_material();
|
||||
m_print->throw_if_canceled();
|
||||
}
|
||||
// Orca: the tree support collision/avoidance caches and support nodes are only used while this step runs
|
||||
// (detect_overhangs() rebuilds them from scratch), so don't keep them resident until the next slice.
|
||||
this->clear_tree_support_preview_cache();
|
||||
// Belt brim rides here rather than in the brim step because its apron
|
||||
// prologue introduces print_z values below the object's first layer, and
|
||||
// those must exist before ToolOrdering is built at psWipeTower - one step
|
||||
|
||||
@@ -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
|
||||
@@ -1532,7 +1532,7 @@ void PrintObject::slice_volumes()
|
||||
if (min_growth < 0.f || elfoot > 0.f) {
|
||||
// Apply the negative XY compensation. (the ones that is <0)
|
||||
ExPolygons trimming;
|
||||
static const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
|
||||
const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
|
||||
if (elfoot > 0.f) {
|
||||
ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps);
|
||||
lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate;
|
||||
|
||||
@@ -1976,37 +1976,15 @@ coordf_t TreeSupport::get_radius(const SupportNode* node)
|
||||
return node->radius;
|
||||
}
|
||||
|
||||
ExPolygons TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
|
||||
// Orca: drop_nodes() calls these several times per node per layer, so they return a reference into the
|
||||
// TreeSupportData cache. The cache only ever inserts, so its entries never move while nodes drop in parallel.
|
||||
const ExPolygons& TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
|
||||
{
|
||||
#if USE_SUPPORT_3D
|
||||
if (m_model_volumes) {
|
||||
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
|
||||
const Polygons& avoid_polys = m_model_volumes->getAvoidance(radius, obj_layer_nr, TreeSupport3D::TreeModelVolumes::AvoidanceType::FastSafe, on_build_plate, true);
|
||||
ExPolygons expolys;
|
||||
for (auto& poly : avoid_polys)
|
||||
expolys.emplace_back(std::move(poly));
|
||||
return expolys;
|
||||
}
|
||||
return ExPolygons();
|
||||
#else
|
||||
return m_ts_data->get_avoidance(radius, obj_layer_nr);
|
||||
#endif
|
||||
}
|
||||
ExPolygons TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
|
||||
const ExPolygons& TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
|
||||
{
|
||||
#if USE_SUPPORT_3D
|
||||
if (m_model_volumes) {
|
||||
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
|
||||
const Polygons& collision_polys = m_model_volumes->getCollision(radius, layer_nr, true);
|
||||
ExPolygons expolys;
|
||||
for (auto& poly : collision_polys)
|
||||
expolys.emplace_back(std::move(poly));
|
||||
return expolys;
|
||||
}
|
||||
#else
|
||||
return m_ts_data->get_collision(radius, layer_nr);
|
||||
#endif
|
||||
return ExPolygons();
|
||||
}
|
||||
Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr)
|
||||
{
|
||||
@@ -2827,7 +2805,12 @@ void TreeSupport::draw_circles()
|
||||
#endif // SUPPORT_TREE_DEBUG_TO_SVG
|
||||
|
||||
SupportLayerPtrs& ts_layers = m_object->support_layers();
|
||||
auto iter = std::remove_if(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return ts_layer->height < EPSILON; });
|
||||
// Orca: the vector owns its layers, so the dropped ones are deleted before they are erased.
|
||||
// std::stable_partition (unlike std::remove_if) leaves exactly the dropped layers in the tail.
|
||||
// Only heights below EPSILON are dropped; written as !(height < EPSILON), a NaN height is kept.
|
||||
auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return !(ts_layer->height < EPSILON); });
|
||||
for (auto it = iter; it != ts_layers.end(); ++it)
|
||||
delete *it;
|
||||
ts_layers.erase(iter, ts_layers.end());
|
||||
for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) {
|
||||
ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr;
|
||||
@@ -3071,7 +3054,7 @@ void TreeSupport::drop_nodes()
|
||||
//Insert a completely new node and let both original nodes fade.
|
||||
Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes.
|
||||
coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next);
|
||||
auto avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
|
||||
const ExPolygons& avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
|
||||
if (group_index == 0)
|
||||
{
|
||||
//Avoid collisions.
|
||||
@@ -3275,7 +3258,7 @@ void TreeSupport::drop_nodes()
|
||||
}
|
||||
#endif
|
||||
coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next);
|
||||
auto avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
|
||||
const ExPolygons& avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
|
||||
|
||||
Point to_outside = projection_onto(avoidance_next, node.position);
|
||||
Point direction_to_outer = to_outside - node.position;
|
||||
@@ -3337,7 +3320,7 @@ void TreeSupport::drop_nodes()
|
||||
p_node->to_buildplate = false;
|
||||
return; // from parallel_for_each lambda
|
||||
}
|
||||
auto next_collision = get_collision(0, obj_layer_nr_next);
|
||||
const ExPolygons& next_collision = get_collision(0, obj_layer_nr_next);
|
||||
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
|
||||
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
|
||||
to_outside = projection_onto(next_collision, next_layer_vertex);
|
||||
|
||||
@@ -536,9 +536,9 @@ private:
|
||||
coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true);
|
||||
coordf_t calc_radius(coordf_t mm_to_top);
|
||||
coordf_t get_radius(const SupportNode* node);
|
||||
ExPolygons get_avoidance(coordf_t radius, size_t obj_layer_nr);
|
||||
const ExPolygons& get_avoidance(coordf_t radius, size_t obj_layer_nr);
|
||||
// layer's expolygon expanded by radius+m_xy_distance
|
||||
ExPolygons get_collision(coordf_t radius, size_t layer_nr);
|
||||
const ExPolygons& get_collision(coordf_t radius, size_t layer_nr);
|
||||
// get Polygons instead of ExPolygons
|
||||
Polygons get_collision_polys(coordf_t radius, size_t layer_nr);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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).
|
||||
@@ -397,14 +457,41 @@ struct TextureDisplacementOptions
|
||||
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
|
||||
int color_despeckle = 2;
|
||||
|
||||
// Where the panel was left, rather than anything the bake reads. It lives with the volume because
|
||||
// that is what it describes: reopening a project should put the user back on the layer they were
|
||||
// editing, in the mode they were editing it in, not on slot 0 in Standard.
|
||||
int active_slot = 0;
|
||||
int panel_mode = 0; // 0 Standard, 1 Pro
|
||||
|
||||
template<class Archive> void serialize(Archive &ar)
|
||||
{
|
||||
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
|
||||
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
|
||||
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
|
||||
v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count,
|
||||
active_slot, panel_mode);
|
||||
}
|
||||
};
|
||||
|
||||
// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the
|
||||
// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and
|
||||
// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would
|
||||
// make every future field a project-breaking change. A JSON object tolerates both directions - an
|
||||
// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and
|
||||
// new ones degrade gracefully in older builds.
|
||||
//
|
||||
// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file
|
||||
// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is
|
||||
// responsible for writing it and for filling `image_data` back in on load.
|
||||
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementOptions &options);
|
||||
// Returns false and leaves both outputs untouched when the text is not a valid JSON object, a number in a
|
||||
// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is
|
||||
// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape
|
||||
// keeps its default.
|
||||
bool texture_displacement_layers_from_json(const std::string &text,
|
||||
std::vector<TextureDisplacementLayer> &layers,
|
||||
TextureDisplacementOptions &options);
|
||||
|
||||
// How much detail a height texture carries: central differences of the grey image, the mean gradient
|
||||
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
|
||||
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
|
||||
@@ -477,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
|
||||
@@ -524,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
|
||||
@@ -541,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 -
|
||||
@@ -578,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
|
||||
@@ -691,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); }
|
||||
|
||||
@@ -749,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
|
||||
@@ -823,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
|
||||
@@ -898,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
|
||||
@@ -1107,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -673,14 +673,14 @@ static bool repair_cluster_smooth(
|
||||
[&]() { return cancelled(); }, &repair_error);
|
||||
if (repaired) {
|
||||
if (cancelled()) return false;
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished.";
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh repair finished.";
|
||||
if (!resample_face_colors(TriMesh(std::move(repaired_its))))
|
||||
return false;
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error;
|
||||
BOOST_LOG_TRIVIAL(warning) << log_prefix << ": mesh repair failed: " << repair_error;
|
||||
}
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair.";
|
||||
BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without repair.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#ifndef libslic3r_Timer_hpp_
|
||||
#define libslic3r_Timer_hpp_
|
||||
#pragma once
|
||||
|
||||
#include "TriangleMesh.hpp"
|
||||
|
||||
@@ -10,5 +9,3 @@ public:
|
||||
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // libslic3r_Timer_hpp_
|
||||
|
||||
@@ -109,7 +109,6 @@ extern std::string format_memsize_MB(size_t n);
|
||||
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
|
||||
// Return string to be added to the boost::log output to inform about the current process memory allocation.
|
||||
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
|
||||
// Latter is used to get the memory info from SysInfoDialog.
|
||||
extern std::string log_memory_info(bool ignore_loglevel = false);
|
||||
extern void disable_multi_threading();
|
||||
// Returns the size of physical memory (RAM) in bytes.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1744,9 +1744,7 @@ std::string format_diameter_to_str(double diameter, int precision)
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
|
||||
// The latter parses the string with (semi)colons as separators, it should look about as
|
||||
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
|
||||
// Returns platform-specific string to be used as log output.
|
||||
std::string log_memory_info(bool ignore_loglevel)
|
||||
{
|
||||
std::string out;
|
||||
@@ -1950,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ namespace libvgcode {
|
||||
// the triangles that share it: the shader runs 8 times per segment instead of 24.
|
||||
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
|
||||
|
||||
static constexpr const std::array<uint8_t, 24> INDEX_DATA = {
|
||||
// 16-bit indices: GCN 1/2 GPUs have no 8-bit index support, and some of their drivers
|
||||
// emulate it wrongly in instanced draws.
|
||||
static constexpr const std::array<uint16_t, 24> INDEX_DATA = {
|
||||
0, 1, 2, // front spike
|
||||
0, 2, 3, // front spike
|
||||
0, 3, 4, // right/bottom body
|
||||
@@ -37,7 +39,7 @@ void SegmentTemplate::init()
|
||||
if (m_vao_id != 0)
|
||||
return;
|
||||
|
||||
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
|
||||
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t) + INDEX_DATA.size() * sizeof(uint16_t);
|
||||
|
||||
int curr_vertex_array;
|
||||
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
||||
@@ -61,7 +63,7 @@ void SegmentTemplate::init()
|
||||
// and restored together with the vao.
|
||||
glsafe(glGenBuffers(1, &m_ibo_id));
|
||||
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
|
||||
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW));
|
||||
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint16_t), INDEX_DATA.data(), GL_STATIC_DRAW));
|
||||
|
||||
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
|
||||
glsafe(glBindVertexArray(curr_vertex_array));
|
||||
@@ -94,7 +96,7 @@ void SegmentTemplate::render(size_t count)
|
||||
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
||||
|
||||
glsafe(glBindVertexArray(m_vao_id));
|
||||
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
|
||||
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_SHORT,
|
||||
nullptr, static_cast<GLsizei>(count)));
|
||||
glsafe(glBindVertexArray(curr_vertex_array));
|
||||
}
|
||||
|
||||
@@ -927,15 +927,17 @@ void ViewerImpl::reset()
|
||||
m_used_extruders.clear();
|
||||
m_total_time = { 0.0f, 0.0f };
|
||||
m_travels_time = { 0.0f, 0.0f };
|
||||
m_vertices.clear();
|
||||
m_vertices_colors.clear();
|
||||
// swap rather than clear: these are sized by the print, and a reset means the memory
|
||||
// should go back, not sit reserved until the next load
|
||||
std::vector<PathVertex>().swap(m_vertices);
|
||||
std::vector<float>().swap(m_vertices_colors);
|
||||
for (std::vector<float>& times : m_layer_start_times)
|
||||
std::vector<float>().swap(times);
|
||||
std::vector<uint32_t>().swap(m_layer_first_vertex);
|
||||
m_layers_in_vertex_order = false;
|
||||
m_valid_lines_bitset.clear();
|
||||
// BitSet::clear() only zeroes the bits, it keeps the blocks allocated; load() builds a new
|
||||
// bitset anyway and it is never read while m_vertices is empty
|
||||
m_valid_lines_bitset = BitSet<>();
|
||||
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
m_cog_marker.reset();
|
||||
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
|
||||
@@ -1834,6 +1836,7 @@ size_t ViewerImpl::get_used_cpu_memory() const
|
||||
ret += sizeof(m_extrusion_roles_colors);
|
||||
ret += sizeof(m_options_colors);
|
||||
ret += STDVEC_MEMSIZE(m_vertices, PathVertex);
|
||||
ret += STDVEC_MEMSIZE(m_vertices_colors, float);
|
||||
for (const std::vector<float>& times : m_layer_start_times)
|
||||
ret += STDVEC_MEMSIZE(times, float);
|
||||
ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t);
|
||||
|
||||
@@ -176,6 +176,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
|
||||
@@ -418,8 +420,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/MultiSendMachineModel.hpp
|
||||
GUI/MultiTaskManagerPage.cpp
|
||||
GUI/MultiTaskManagerPage.hpp
|
||||
GUI/MultiTaskModel.cpp
|
||||
GUI/MultiTaskModel.hpp
|
||||
GUI/NetworkTestDialog.cpp
|
||||
GUI/NetworkTestDialog.hpp
|
||||
GUI/Notebook.cpp
|
||||
@@ -804,7 +804,6 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/PrintHost.hpp
|
||||
Utils/Process.cpp
|
||||
Utils/Process.hpp
|
||||
Utils/Profile.hpp
|
||||
Utils/RaycastManager.cpp
|
||||
Utils/RaycastManager.hpp
|
||||
Utils/Repetier.cpp
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -122,7 +122,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});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#ifndef slic3r_GUI_SingleChoice_hpp_
|
||||
#define slic3r_GUI_SingleChoice_hpp_
|
||||
#pragma once
|
||||
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "Plater.hpp"
|
||||
@@ -33,5 +32,3 @@ private:
|
||||
|
||||
};
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "libslic3r/Exception.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ConfigError : public Slic3r::RuntimeError {
|
||||
using Slic3r::RuntimeError::RuntimeError;
|
||||
};
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class ConfigGUITypeError : public ConfigError {
|
||||
using ConfigError::ConfigError;
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -1105,6 +1105,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
toggle_field("support_filament", have_support_material || have_skirt);
|
||||
|
||||
toggle_line("raft_contact_distance", have_raft && !have_support_soluble);
|
||||
// Orca:Hide raft expansion when raft is not enabled
|
||||
toggle_line("raft_expansion", have_raft);
|
||||
// Orca: Slim, Strong and Hybrid tree supports size their raft by first layer expansion only.
|
||||
toggle_field("raft_expansion", !(is_tree(support_type) && (support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid)));
|
||||
|
||||
// Orca: First-layer density is available for supports broadly.
|
||||
toggle_field("raft_first_layer_density", have_support_material);
|
||||
|
||||
@@ -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'))) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
@@ -66,6 +67,7 @@
|
||||
#include <vector>
|
||||
#include <wx/colour.h>
|
||||
#include <tuple>
|
||||
#include <wx/app.h>
|
||||
#include <wx/dir.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/event.h>
|
||||
@@ -527,6 +529,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;
|
||||
@@ -535,6 +549,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) {
|
||||
@@ -548,25 +563,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())
|
||||
@@ -2678,7 +2678,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();
|
||||
|
||||
@@ -2691,17 +2691,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{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1514,6 +1514,8 @@ wxDEFINE_EVENT(EVT_GLCANVAS_PRINTABLE, SimpleEvent);
|
||||
const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25;
|
||||
const double GLCanvas3D::DefaultCameraZoomToBedMarginFactor = 2.00;
|
||||
const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25;
|
||||
// Main-thread only: _render_frame never runs off it, so no atomic is needed.
|
||||
int GLCanvas3D::s_frames_in_progress = 0;
|
||||
|
||||
void GLCanvas3D::load_arrange_settings()
|
||||
{
|
||||
@@ -1637,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")},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2468,7 +2470,8 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
|
||||
}
|
||||
|
||||
m_in_render = true;
|
||||
Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; });
|
||||
++s_frames_in_progress;
|
||||
Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; --s_frames_in_progress; });
|
||||
(void)in_render_guard;
|
||||
|
||||
if (m_canvas == nullptr)
|
||||
@@ -2713,15 +2716,31 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
|
||||
|
||||
// On Wayland, eglSwapBuffers blocks when the canvas is hidden or
|
||||
// occluded. Skip the swap to avoid stalling the render loop.
|
||||
if (m_canvas->IsShownOnScreen()) {
|
||||
// Code run from inside this frame - a modal dialog a gizmo opened from its ImGui panel, a
|
||||
// synchronous paint of another canvas - may have made a different surface current. Bind this one
|
||||
// again before presenting, so the swap is not issued against a window the context no longer
|
||||
// belongs to: on Mesa/X11 that is a fatal BadMatch, not the no-op it is on other platforms. Draws
|
||||
// already issued before this point cannot be redirected - only this swap is protected.
|
||||
if (m_canvas->IsShownOnScreen() && _set_current()) {
|
||||
m_canvas->SwapBuffers();
|
||||
m_frame_profiler.mark("swap");
|
||||
if (!overlay_tick)
|
||||
m_render_stats.increment_fps_counter();
|
||||
m_presented_signature = signature;
|
||||
}
|
||||
else
|
||||
else {
|
||||
if (m_canvas->IsShownOnScreen()) {
|
||||
// Logged once: if the rebind fails it does so every frame, and a warning per frame would
|
||||
// bury everything else.
|
||||
static bool rebind_failure_logged = false;
|
||||
if (!rebind_failure_logged) {
|
||||
rebind_failure_logged = true;
|
||||
BOOST_LOG_TRIVIAL(warning) << "GLCanvas3D: could not rebind the GL context to this canvas before "
|
||||
"SwapBuffers(); the frame is not presented";
|
||||
}
|
||||
}
|
||||
m_presented_signature.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Everything drawn into the 3D scene, from the clear to the post processing passes, ending in the
|
||||
@@ -3034,10 +3053,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;
|
||||
}
|
||||
|
||||
@@ -4960,7 +4978,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);
|
||||
@@ -4970,7 +4988,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;
|
||||
@@ -5502,7 +5531,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;
|
||||
@@ -5534,10 +5563,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());
|
||||
@@ -5546,14 +5583,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();
|
||||
@@ -5570,13 +5599,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);
|
||||
}
|
||||
@@ -5585,9 +5615,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;
|
||||
}
|
||||
|
||||
@@ -5716,9 +5745,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;
|
||||
}
|
||||
|
||||
@@ -5819,9 +5847,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;
|
||||
}
|
||||
|
||||
@@ -5940,9 +5967,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;
|
||||
}
|
||||
|
||||
@@ -7948,8 +7974,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);
|
||||
}
|
||||
@@ -11176,7 +11202,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);
|
||||
}
|
||||
}
|
||||
@@ -12067,12 +12093,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;
|
||||
@@ -12120,11 +12143,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";
|
||||
@@ -12151,7 +12172,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;
|
||||
}
|
||||
|
||||
@@ -643,6 +643,8 @@ private:
|
||||
#endif
|
||||
unsigned int m_last_w, m_last_h;
|
||||
bool m_in_render;
|
||||
// How many canvases are inside _render_frame() right now - see is_frame_in_progress().
|
||||
static int s_frames_in_progress;
|
||||
wxTimer m_timer;
|
||||
wxTimer m_timer_set_color;
|
||||
// Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame.
|
||||
@@ -974,6 +976,11 @@ public:
|
||||
void on_change_color_mode(bool is_dark, bool reinit = true);
|
||||
const bool get_dark_mode_status() { return m_is_dark; }
|
||||
void set_as_dirty() { m_dirty = true; }
|
||||
// True while any GLCanvas3D is inside _render_frame(). The other surfaces that share the app's one
|
||||
// GL context (the UV editor canvas) check this before making themselves current: a frame interrupted
|
||||
// by another surface finishes drawing into that surface and then presents a window the context no
|
||||
// longer belongs to, which on Mesa/X11 is a fatal X error (BadMatch) rather than a no-op.
|
||||
static bool is_frame_in_progress() { return s_frames_in_progress > 0; }
|
||||
void requires_check_outside_state() { m_requires_check_outside_state = true; }
|
||||
|
||||
unsigned int get_volumes_count() const { return (unsigned int)m_volumes.volumes.size(); }
|
||||
@@ -1298,7 +1305,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();
|
||||
|
||||
@@ -719,9 +719,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
|
||||
// does not treat anti-aliased icon edges as window transparency.
|
||||
glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
|
||||
|
||||
glsafe(::glEnable(GL_TEXTURE_2D));
|
||||
glsafe(::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE));
|
||||
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id));
|
||||
|
||||
GLModel::Geometry init_data;
|
||||
@@ -753,7 +750,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
|
||||
|
||||
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
|
||||
|
||||
glsafe(::glDisable(GL_TEXTURE_2D));
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
|
||||
+156
-99
@@ -1109,6 +1109,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 +1149,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 +1808,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) {
|
||||
@@ -2616,7 +2606,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 +3542,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 +3563,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 +3916,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 +4011,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 +4220,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 +4265,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 +4294,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 +4303,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://");
|
||||
|
||||
@@ -4862,12 +4857,6 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
|
||||
}
|
||||
|
||||
void GUI_App::system_info()
|
||||
{
|
||||
//SysInfoDialog dlg;
|
||||
//dlg.ShowModal();
|
||||
}
|
||||
|
||||
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
|
||||
{
|
||||
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
|
||||
@@ -6822,6 +6811,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()
|
||||
{
|
||||
@@ -8588,22 +8642,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,
|
||||
@@ -8612,7 +8667,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();
|
||||
}
|
||||
@@ -10078,7 +10133,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);
|
||||
|
||||
@@ -519,7 +519,6 @@ public:
|
||||
void check_printer_presets();
|
||||
|
||||
void recreate_GUI(const wxString& message);
|
||||
void system_info();
|
||||
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
|
||||
void troubleshoot();
|
||||
void load_project(wxWindow *parent, wxString& input_file) const;
|
||||
@@ -568,6 +567,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();
|
||||
|
||||
@@ -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();
|
||||
@@ -1952,6 +1957,16 @@ void MenuFactory::create_plate_menu()
|
||||
},
|
||||
"", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent);
|
||||
|
||||
// fix objects on current plate
|
||||
append_menu_item(menu, wxID_ANY, _L("Fix All"), _L("Fix all models on the current plate"),
|
||||
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(false); }, "", nullptr,
|
||||
[]() { return !plater()->get_partplate_list().get_selected_plate()->empty(); }, m_parent);
|
||||
|
||||
// fix objects on all plates
|
||||
append_menu_item(menu, wxID_ANY, _L("Fix All Plates"), _L("Fix all models on all plates"),
|
||||
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(true); }, "", nullptr,
|
||||
[]() { return !plater()->model().objects.empty(); }, m_parent);
|
||||
|
||||
// orient objects on current plate
|
||||
append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"),
|
||||
[](wxCommandEvent&) {
|
||||
@@ -2126,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);
|
||||
@@ -2134,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
|
||||
@@ -2151,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);
|
||||
@@ -2315,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") , "",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -6297,30 +6297,44 @@ void ObjectList::rename_item()
|
||||
}
|
||||
|
||||
void ObjectList::fix_through_cgal()
|
||||
{
|
||||
std::vector<int> obj_idxs, vol_idxs;
|
||||
get_selection_indexes(obj_idxs, vol_idxs);
|
||||
fix_through_cgal(obj_idxs, vol_idxs);
|
||||
}
|
||||
|
||||
void ObjectList::fix_all_through_cgal(bool all_plates)
|
||||
{
|
||||
std::vector<int> obj_idxs;
|
||||
if (all_plates) {
|
||||
for (int obj_idx = 0; obj_idx < int(m_objects->size()); ++obj_idx)
|
||||
obj_idxs.push_back(obj_idx);
|
||||
} else {
|
||||
for (const ModelObject *mo : wxGetApp().plater()->get_partplate_list().get_selected_plate()->get_objects_on_this_plate())
|
||||
obj_idxs.push_back(int(std::find(m_objects->begin(), m_objects->end(), mo) - m_objects->begin()));
|
||||
std::sort(obj_idxs.begin(), obj_idxs.end());
|
||||
obj_idxs.erase(std::unique(obj_idxs.begin(), obj_idxs.end()), obj_idxs.end());
|
||||
}
|
||||
fix_through_cgal(obj_idxs, {});
|
||||
}
|
||||
|
||||
void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs)
|
||||
{
|
||||
// Do not fix anything when a gizmo is open. There might be issues with updates
|
||||
// and what is worse, the snapshot time would refer to the internal stack.
|
||||
if (!wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
|
||||
if (obj_idxs.empty() || !wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
|
||||
return;
|
||||
|
||||
// model_name
|
||||
std::vector<std::string> succes_models;
|
||||
// model_name failing reason
|
||||
std::vector<std::pair<std::string, std::string>> failed_models;
|
||||
|
||||
std::vector<int> obj_idxs, vol_idxs;
|
||||
get_selection_indexes(obj_idxs, vol_idxs);
|
||||
size_t unchanged_models = 0;
|
||||
bool painting_removed = false;
|
||||
|
||||
std::vector<std::string> model_names;
|
||||
|
||||
// clear selections from the non-broken models if any exists
|
||||
// and than fill names of models to repairing
|
||||
if (vol_idxs.empty()) {
|
||||
#if !FIX_THROUGH_CGAL_ALWAYS
|
||||
for (int i = int(obj_idxs.size())-1; i >= 0; --i)
|
||||
if (object(obj_idxs[i])->get_repaired_errors_count() == 0)
|
||||
obj_idxs.erase(obj_idxs.begin()+i);
|
||||
#endif // FIX_THROUGH_CGAL_ALWAYS
|
||||
for (int obj_idx : obj_idxs)
|
||||
if (object(obj_idx))
|
||||
model_names.push_back(object(obj_idx)->name);
|
||||
@@ -6328,31 +6342,26 @@ void ObjectList::fix_through_cgal()
|
||||
else {
|
||||
ModelObject* obj = object(obj_idxs.front());
|
||||
if (obj) {
|
||||
#if !FIX_THROUGH_CGAL_ALWAYS
|
||||
for (int i = int(vol_idxs.size()) - 1; i >= 0; --i)
|
||||
if (obj->get_repaired_errors_count(vol_idxs[i]) == 0)
|
||||
vol_idxs.erase(vol_idxs.begin() + i);
|
||||
#endif // FIX_THROUGH_CGAL_ALWAYS
|
||||
for (int vol_idx : vol_idxs)
|
||||
model_names.push_back(obj->volumes[vol_idx]->name);
|
||||
}
|
||||
}
|
||||
|
||||
auto plater = wxGetApp().plater();
|
||||
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
|
||||
|
||||
auto fix_and_update_progress = [this, plater, model_names](const int obj_idx, const int vol_idx,
|
||||
int model_idx,
|
||||
ProgressDialog& progress_dlg,
|
||||
std::vector<std::string>& succes_models,
|
||||
std::vector<std::pair<std::string, std::string>>& failed_models)
|
||||
auto fix_and_update_progress = [&](const int obj_idx, const int vol_idx, int model_idx, ProgressDialog& progress_dlg)
|
||||
{
|
||||
if (!object(obj_idx))
|
||||
ModelObject *mo = object(obj_idx);
|
||||
if (!mo)
|
||||
return false;
|
||||
|
||||
const std::string& model_name = model_names[model_idx];
|
||||
wxString msg = _L("Repairing model object");
|
||||
if (model_names.size() == 1)
|
||||
msg += ": " + from_u8(model_name) + "\n";
|
||||
else if (model_names.size() > 10)
|
||||
msg += ": " + from_u8(model_name) + wxString::Format(" (%d/%d)", model_idx + 1, int(model_names.size())) + "\n";
|
||||
else {
|
||||
msg += ":\n";
|
||||
for (int i = 0; i < int(model_names.size()); ++i)
|
||||
@@ -6360,33 +6369,29 @@ void ObjectList::fix_through_cgal()
|
||||
msg += "\n";
|
||||
}
|
||||
|
||||
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
|
||||
if (!keep_painting) {
|
||||
plater->clear_before_change_mesh(obj_idx);
|
||||
}
|
||||
const size_t volumes_before = object(obj_idx)->volumes.size();
|
||||
auto count_painted = [mo]() { return std::count_if(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume *mv) { return mv->is_any_painted(); }); };
|
||||
const auto painted_before = count_painted();
|
||||
std::string res;
|
||||
if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting))
|
||||
bool modified = false;
|
||||
if (!fix_model_with_cgal_gui(*mo, vol_idx, progress_dlg, msg, res, keep_painting, &modified))
|
||||
return false;
|
||||
//wxGetApp().plater()->changed_mesh(obj_idx);
|
||||
object(obj_idx)->ensure_on_bed();
|
||||
plater->changed_mesh(obj_idx);
|
||||
|
||||
const size_t volumes_after = object(obj_idx)->volumes.size();
|
||||
if (volumes_after != volumes_before)
|
||||
add_volumes_to_object_in_list(obj_idx);
|
||||
if (modified) {
|
||||
painting_removed = painting_removed || count_painted() < painted_before;
|
||||
mo->ensure_on_bed();
|
||||
plater->changed_mesh(obj_idx);
|
||||
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
|
||||
plater->sidebar().obj_list()->update_plate_values_for_items();
|
||||
update_item_error_icon(obj_idx, vol_idx);
|
||||
update_info_items(obj_idx);
|
||||
}
|
||||
|
||||
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
|
||||
plater->sidebar().obj_list()->update_plate_values_for_items();
|
||||
|
||||
if (res.empty())
|
||||
if (!res.empty())
|
||||
failed_models.push_back({ model_name, res });
|
||||
else if (modified)
|
||||
succes_models.push_back(model_name);
|
||||
else
|
||||
failed_models.push_back({ model_name, res });
|
||||
|
||||
update_item_error_icon(obj_idx, vol_idx);
|
||||
update_info_items(obj_idx);
|
||||
|
||||
++unchanged_models;
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -6399,11 +6404,7 @@ void ObjectList::fix_through_cgal()
|
||||
if (vol_idxs.empty()) {
|
||||
int vol_idx{ -1 };
|
||||
for (int obj_idx : obj_idxs) {
|
||||
#if !FIX_THROUGH_CGAL_ALWAYS
|
||||
if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0)
|
||||
continue;
|
||||
#endif // FIX_THROUGH_CGAL_ALWAYS
|
||||
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
|
||||
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
|
||||
break;
|
||||
model_idx++;
|
||||
}
|
||||
@@ -6411,7 +6412,7 @@ void ObjectList::fix_through_cgal()
|
||||
else {
|
||||
int obj_idx{ obj_idxs.front() };
|
||||
for (int vol_idx : vol_idxs) {
|
||||
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
|
||||
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
|
||||
break;
|
||||
model_idx++;
|
||||
}
|
||||
@@ -6419,20 +6420,29 @@ void ObjectList::fix_through_cgal()
|
||||
// Close the progress dialog
|
||||
progress_dlg.Update(100, "");
|
||||
|
||||
if (painting_removed)
|
||||
plater->notify_painting_removed();
|
||||
|
||||
// Show info notification
|
||||
wxString msg;
|
||||
std::vector<wxString> blocks;
|
||||
wxString bullet_suf = "\n - ";
|
||||
if (!succes_models.empty()) {
|
||||
msg = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
|
||||
wxString block = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
|
||||
for (auto& model : succes_models)
|
||||
msg += bullet_suf + from_u8(model);
|
||||
msg += "\n\n";
|
||||
block += bullet_suf + from_u8(model);
|
||||
blocks.push_back(block);
|
||||
}
|
||||
if (!failed_models.empty()) {
|
||||
msg += _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":\n";
|
||||
wxString block = _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":";
|
||||
for (auto& model : failed_models)
|
||||
msg += bullet_suf + from_u8(model.first) + ": " + _(model.second);
|
||||
block += bullet_suf + from_u8(model.first) + ": " + _(model.second);
|
||||
blocks.push_back(block);
|
||||
}
|
||||
if (unchanged_models > 0)
|
||||
blocks.push_back(format_wxstr(_L_PLURAL("%1$d model object needed no repair.", "%1$d model objects needed no repair.", unchanged_models), unchanged_models));
|
||||
wxString msg;
|
||||
for (const wxString &block : blocks)
|
||||
msg += (msg.IsEmpty() ? "" : "\n\n") + block;
|
||||
if (msg.IsEmpty())
|
||||
msg = _L("Repairing was canceled");
|
||||
plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg));
|
||||
@@ -7088,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());
|
||||
|
||||
@@ -47,9 +47,6 @@ typedef double coordf_t;
|
||||
typedef std::pair<coordf_t, coordf_t> t_layer_height_range;
|
||||
typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
|
||||
|
||||
// Manifold mesh may contain self-intersections, so we want to always allow fixing the mesh.
|
||||
#define FIX_THROUGH_CGAL_ALWAYS 1
|
||||
|
||||
namespace GUI {
|
||||
|
||||
enum class Shortcut : uint8_t;
|
||||
@@ -463,6 +460,9 @@ public:
|
||||
void split_instances();
|
||||
void rename_item();
|
||||
void fix_through_cgal();
|
||||
// Fixes every object on the selected plate, or on all plates.
|
||||
void fix_all_through_cgal(bool all_plates);
|
||||
void fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs);
|
||||
void simplify();
|
||||
void smooth_mesh();
|
||||
void update_item_error_icon(const int obj_idx, int vol_idx) const ;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user