mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl
This commit is contained in:
@@ -73,6 +73,7 @@ using namespace nlohmann;
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/BlacklistedLibraryCheck.hpp"
|
||||
#include "libslic3r/FlushVolCalc.hpp"
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
|
||||
#include "libslic3r/Orient.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
@@ -85,6 +86,7 @@ using namespace nlohmann;
|
||||
#ifdef WIN32
|
||||
#include "dev-utils/BaseException.h"
|
||||
#endif
|
||||
#include "slic3r/Utils/MeshInspect.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/BitmapCache.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
@@ -189,6 +191,9 @@ typedef struct _sliced_info {
|
||||
int wall_loops{0};
|
||||
std::vector<std::string> upward_machines;
|
||||
std::vector<std::string> downward_machines;
|
||||
// Structured slicing warnings for result.json, and whether --strict was on.
|
||||
nlohmann::json warnings = nlohmann::json::array();
|
||||
bool strict_mode {false};
|
||||
}sliced_info_t;
|
||||
std::vector<PrintBase::SlicingStatus> g_slicing_warnings;
|
||||
|
||||
@@ -424,6 +429,21 @@ static PrinterTechnology get_printer_technology(const DynamicConfig &config)
|
||||
return(ret);}
|
||||
#endif
|
||||
|
||||
// Records a structured slicing warning so a CI or scripted consumer can branch on
|
||||
// a stable `class` string instead of matching stderr. Warnings are kept on the
|
||||
// run's sliced_info and emitted as the top-level "warnings" array of result.json;
|
||||
// a non-empty array does not by itself mean the run failed. Under --strict a
|
||||
// NON_CRITICAL warning additionally ends the run non-zero.
|
||||
//
|
||||
// result.json is written on Linux only (see the guard in record_exit_reson), so
|
||||
// neither "warnings" nor "strict_mode" reaches Windows or macOS.
|
||||
static void cli_record_warning(sliced_info_t &sliced_info, const std::string &cls,
|
||||
nlohmann::json details = nlohmann::json::object())
|
||||
{
|
||||
details["class"] = cls;
|
||||
sliced_info.warnings.push_back(std::move(details));
|
||||
}
|
||||
|
||||
void record_exit_reson(std::string outputdir, int code, int plate_id, std::string error_message, sliced_info_t& sliced_info, std::map<std::string, std::string> key_values = std::map<std::string, std::string>())
|
||||
{
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
@@ -462,6 +482,9 @@ void record_exit_reson(std::string outputdir, int code, int plate_id, std::strin
|
||||
for (auto& iter: key_values)
|
||||
j[iter.first] = iter.second;
|
||||
|
||||
j["warnings"] = sliced_info.warnings;
|
||||
j["strict_mode"] = sliced_info.strict_mode;
|
||||
|
||||
boost::nowide::ofstream c;
|
||||
c.open(result_file, std::ios::out | std::ios::trunc);
|
||||
c << j.dump(1, '\t') << std::endl;
|
||||
@@ -1381,12 +1404,45 @@ int CLI::run(int argc, char **argv)
|
||||
bool need_skip = (skip_objects.size() > 0)?true:false;
|
||||
long long global_begin_time = 0, global_current_time;
|
||||
sliced_info_t sliced_info;
|
||||
// Read up front so result.json reports it for early failures too.
|
||||
sliced_info.strict_mode = m_config.opt_bool("strict");
|
||||
// --no-check skips the check behind the only NON_CRITICAL warning --strict acts on
|
||||
// (support needed but disabled), from the point it appears among the actions. The pair
|
||||
// would make --strict a no-op or depend on argument order, so refuse it.
|
||||
if (sliced_info.strict_mode && m_config.opt_bool("no_check")) {
|
||||
boost::nowide::cerr << "--strict cannot be combined with --no-check" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
std::map<std::string, std::string> record_key_values;
|
||||
|
||||
ConfigOptionBool* downward_check_option = m_config.option<ConfigOptionBool>("downward_check");
|
||||
if (downward_check_option)
|
||||
downward_check = downward_check_option->value;
|
||||
|
||||
// --inspect-mesh prints its JSON and exits, so any action that does work of its
|
||||
// own (slicing, exporting) would be skipped without notice. Reject those up front;
|
||||
// only options that merely tune how the input is loaded may come along.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "inspect_mesh") != m_actions.end()) {
|
||||
static const std::set<std::string> inspect_compatible = { "inspect_mesh", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::string &action : m_actions) {
|
||||
if (inspect_compatible.count(action) == 0) {
|
||||
std::string flag = action;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--inspect-mesh cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
|
||||
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
|
||||
boost::nowide::cerr << "--inspect-mesh needs an input file or --load-assemble-list" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
|
||||
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
|
||||
// sliced or exported.
|
||||
@@ -4810,6 +4866,64 @@ int CLI::run(int argc, char **argv)
|
||||
for (auto &o : model.objects)
|
||||
// this affects volumes:
|
||||
o->rotate(Geometry::deg2rad(m_config.opt_float(opt_key)), Y);
|
||||
} else if (opt_key == "ground_largest_face" || opt_key == "ground_face_normal" || opt_key == "ground_face_point") {
|
||||
// Each instance is laid on one of its lay-on-face planes, which are computed from the current part
|
||||
// transformations, so the rotations given before this option are respected. A direction or point is in
|
||||
// object coordinates, so it names the same face for every instance of an object.
|
||||
std::function<int(const std::vector<LayOnFacePlane>&, const Transform3d&)> pick;
|
||||
if (opt_key == "ground_largest_face") {
|
||||
if (m_config.opt_bool(opt_key))
|
||||
pick = [](const std::vector<LayOnFacePlane>& planes, const Transform3d&) { return find_largest_plane(planes); };
|
||||
} else {
|
||||
// Only options given on the command line reach this loop, so an empty value is malformed input too.
|
||||
const std::string& value = m_config.opt_string(opt_key);
|
||||
Vec3d v;
|
||||
int consumed = 0;
|
||||
if (sscanf(value.c_str(), "%lf,%lf,%lf%n", &v.x(), &v.y(), &v.z(), &consumed) != 3 || consumed != int(value.size()) ||
|
||||
!v.allFinite() || (opt_key == "ground_face_normal" && v.norm() < EPSILON)) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Invalid params: %1% expects three comma-separated numbers, got \"%2%\"") % opt_key % value;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
if (opt_key == "ground_face_normal")
|
||||
pick = [v](const std::vector<LayOnFacePlane>& planes, const Transform3d&) { return find_plane_by_normal(planes, v); };
|
||||
else
|
||||
pick = [v](const std::vector<LayOnFacePlane>& planes, const Transform3d& inst_matrix) {
|
||||
return find_plane_at_point(planes, inst_matrix, v, 0.01);
|
||||
};
|
||||
}
|
||||
if (pick) {
|
||||
size_t laid = 0, missed = 0;
|
||||
for (auto& model : m_models) {
|
||||
model.add_default_instances();
|
||||
for (ModelObject* o : model.objects)
|
||||
for (size_t i = 0; i < o->instances.size(); ++i) {
|
||||
const Transform3d inst_matrix = o->instances[i]->get_matrix_no_offset();
|
||||
const std::vector<LayOnFacePlane> planes = lay_on_face_planes(*o, inst_matrix);
|
||||
if (planes.empty()) {
|
||||
// Small or smooth parts (e.g. a sphere) have no face to rest on; the gizmo offers none either.
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: object %2% has no face large enough to lay on, left as it is") % opt_key % o->name;
|
||||
continue;
|
||||
}
|
||||
const int idx = pick(planes, inst_matrix);
|
||||
if (idx < 0) {
|
||||
// Only a point can miss: with several objects it usually belongs to one of them.
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: no face of object %2% contains the point, left as it is") % opt_key % o->name;
|
||||
++missed;
|
||||
continue;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: object %2% instance %3% laid on the %4% mm2 face with normal %5%")
|
||||
% opt_key % o->name % i % planes[idx].area % planes[idx].normal.transpose();
|
||||
lay_on_face(*o, i, planes[idx].normal);
|
||||
++laid;
|
||||
}
|
||||
}
|
||||
if (laid == 0 && missed > 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Invalid params: %1%: no face of any object contains the point") % opt_key;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "scale") {
|
||||
float ratio = m_config.opt_float(opt_key);
|
||||
if (ratio <= 0.f) {
|
||||
@@ -5969,6 +6083,29 @@ int CLI::run(int argc, char **argv)
|
||||
model.add_default_instances();
|
||||
model.print_info();
|
||||
}
|
||||
} else if (opt_key == "inspect_mesh") {
|
||||
// Machine-readable alternative to --info. Registered as an action so it satisfies the
|
||||
// "needs an action" check and bypasses the GUI fallback, then exits once the JSON is out.
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
Slic3r::MeshInspect::inspect_to_json(model, m_input_files, boost::nowide::cout);
|
||||
}
|
||||
boost::nowide::cout.flush();
|
||||
// Conflicting actions were rejected before loading. Finish like the end of run().
|
||||
// flush_and_exit() is not usable here: it prints "found error ..." to stdout,
|
||||
// which would corrupt the JSON.
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
if (g_cli_callback_mgr.is_started()) {
|
||||
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
|
||||
boost::nowide::cerr.flush();
|
||||
return CLI_SUCCESS;
|
||||
} else if (opt_key == "uptodate") {
|
||||
//already processed before
|
||||
} else if (opt_key == "min_save") {
|
||||
@@ -6009,6 +6146,8 @@ int CLI::run(int argc, char **argv)
|
||||
export_3mf_file = m_config.opt_string(opt_key);
|
||||
}else if(opt_key=="no_check"){
|
||||
no_check = m_config.opt_bool(opt_key);
|
||||
}else if(opt_key=="strict"){
|
||||
//already read into sliced_info at the start of run()
|
||||
//} else if (opt_key == "export_gcode" || opt_key == "export_sla" || opt_key == "slice") {
|
||||
} else if (opt_key == "normative_check") {
|
||||
//already processed before
|
||||
@@ -6717,6 +6856,15 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
if (status.warning_level == PrintStateBase::WarningLevel::NON_CRITICAL) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "plate "<< index+1<< ": found NON_CRITICAL slicing warnings: "<<status.text <<std::endl;
|
||||
// Always record for AI/CI consumers; under --strict, elevate to a
|
||||
// non-zero exit so scripted pipelines don't ship a "warning OK" slice.
|
||||
cli_record_warning(sliced_info, "slicing_warning_non_critical",
|
||||
nlohmann::json{{"plate_id", index+1}, {"text", status.text}});
|
||||
if (sliced_info.strict_mode) {
|
||||
sliced_info.sliced_plates.push_back(sliced_plate_info);
|
||||
record_exit_reson(outfile_dir, CLI_SLICING_ERROR, index+1, cli_errors[CLI_SLICING_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_SLICING_ERROR);
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("plate %1%: found slicing warnings: %2%, no_check=%3%")%(index+1) %status.text %no_check;
|
||||
|
||||
@@ -260,6 +260,8 @@ set(lisbslic3r_sources
|
||||
GCode/SmallAreaInfillFlowCompensator.hpp
|
||||
GCode/SpiralVase.cpp
|
||||
GCode/SpiralVase.hpp
|
||||
GCode/WipePathHelpers.cpp
|
||||
GCode/WipePathHelpers.hpp
|
||||
GCode/ThumbnailData.cpp
|
||||
GCode/ThumbnailData.hpp
|
||||
GCode/Thumbnails.cpp
|
||||
@@ -302,6 +304,8 @@ set(lisbslic3r_sources
|
||||
Layer.cpp
|
||||
Layer.hpp
|
||||
LayerRegion.cpp
|
||||
LayOnFace.cpp
|
||||
LayOnFace.hpp
|
||||
libslic3r.cpp
|
||||
libslic3r.h
|
||||
Line.cpp
|
||||
|
||||
+119
-79
@@ -1,5 +1,6 @@
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "GCode/WipePathHelpers.hpp"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
@@ -438,7 +439,6 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
auto& writer = gcodegen.writer();
|
||||
auto& config = gcodegen.config();
|
||||
auto extruder = writer.filament();
|
||||
auto extruder_id = extruder->extruder_id();
|
||||
auto last_pos = gcodegen.last_pos();
|
||||
|
||||
// Declare & initialize retraction lengths
|
||||
@@ -475,13 +475,13 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
wipe_speed = std::max(wipe_speed, 10.0);
|
||||
|
||||
// Process wipe path & calculate wipe path length
|
||||
double wipe_dist = scale_(config.wipe_distance.get_at(extruder_id));
|
||||
double wipe_dist = scale_(config.wipe_distance.get_at(extruder->config_index()));
|
||||
Polyline wipe_path = {last_pos};
|
||||
wipe_path.append(this->path.points.begin() + 1, this->path.points.end());
|
||||
double wipe_path_length = std::min(wipe_path.length(), wipe_dist);
|
||||
|
||||
// Calculate the maximum retraction amount during wipe
|
||||
retraction_length_during_wipe = config.retraction_speed.get_at(extruder_id) *
|
||||
retraction_length_during_wipe = config.retraction_speed.get_at(extruder->config_index()) *
|
||||
unscale_(wipe_path_length) / wipe_speed;
|
||||
|
||||
// If the maximum retraction amount during wipe is too small,
|
||||
@@ -564,6 +564,16 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return default_value;
|
||||
}
|
||||
|
||||
// Orca: rebuild the stored wipe path while preserving Polyline's boundary deduplication.
|
||||
void Wipe::update_path(const ExtrusionPaths &paths, bool reverse)
|
||||
{
|
||||
reset_path();
|
||||
for (const ExtrusionPath& extrusion_path : paths)
|
||||
path.append(extrusion_path.polyline.to_polyline());
|
||||
if (reverse)
|
||||
path.reverse();
|
||||
}
|
||||
|
||||
std::string Wipe::wipe(GCode& gcodegen,double length, bool toolchange, bool is_last)
|
||||
{
|
||||
std::string gcode;
|
||||
@@ -616,14 +626,11 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
if (gcodegen.enable_cooling_markers() && !is_last)
|
||||
cooling_mark = /*gcodegen.config().role_based_wipe_speed ? ";_EXTERNAL_PERIMETER" : */";_WIPE";
|
||||
|
||||
// Orca: set speed once because wipe_speed is constant for all segments.
|
||||
gcode += gcodegen.writer().set_speed(_wipe_speed * 60, "", cooling_mark);
|
||||
for (const Line& line : wipe_path.lines()) {
|
||||
double segment_length = line.length();
|
||||
double dE = length * (segment_length / wipe_dist);
|
||||
//BBS: fix this FIXME
|
||||
//FIXME one shall not generate the unnecessary G1 Fxxx commands, here wipe_speed is a constant inside this cycle.
|
||||
// Is it here for the cooling markers? Or should it be outside of the cycle?
|
||||
//gcode += gcodegen.writer().set_speed(wipe_speed * 60, "", gcodegen.enable_cooling_markers() ? ";_WIPE" : "");
|
||||
gcode += gcodegen.writer().extrude_to_xy(
|
||||
gcodegen.point_to_gcode(line.b),
|
||||
-dE,
|
||||
@@ -2901,6 +2908,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
const bool skip_config_block = print.config().gcode_skip_config_block;
|
||||
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
|
||||
m_calib_config.clear();
|
||||
// Orca: Calibration overrides are reapplied after object/region settings in _extrude().
|
||||
// Keep inward wiping from masking retraction and pressure advance artifacts.
|
||||
switch (print.calib_mode()) {
|
||||
case CalibMode::Calib_PA_Line:
|
||||
case CalibMode::Calib_PA_Pattern:
|
||||
case CalibMode::Calib_PA_Tower:
|
||||
case CalibMode::Calib_Auto_PA_Line:
|
||||
case CalibMode::Calib_Retraction_tower:
|
||||
m_calib_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// resets analyzer's tracking data
|
||||
m_last_height = 0.f;
|
||||
m_last_layer_z = 0.f;
|
||||
@@ -7204,7 +7224,8 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
|
||||
const std::string& description,
|
||||
double speed,
|
||||
const ExtrusionEntitiesPtr& region_perimeters,
|
||||
const Point* start_point)
|
||||
const Point* start_point,
|
||||
const WipeInwardSupport* wipe_support)
|
||||
{
|
||||
// get a copy; don't modify the orientation of the original loop object otherwise
|
||||
// next copies (if any) would not detect the correct orientation
|
||||
@@ -7434,63 +7455,80 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
|
||||
m_processor.result().print_statistics.total_seam_scarf_distance += static_cast<float>(seam_scarf_distance_mm);
|
||||
}
|
||||
|
||||
// BBS
|
||||
// Orca: share the post-extrusion nozzle position between wipe_inward and wipe_on_loops.
|
||||
const bool is_ccw = loop.is_counter_clockwise();
|
||||
|
||||
std::optional<Point> wipe_on_loops_dest;
|
||||
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter &&
|
||||
m_layer != nullptr && m_config.wall_loops.value > 1 && paths.front().size() >= 2 &&
|
||||
paths.back().polyline.points.size() >= 2)
|
||||
wipe_on_loops_dest = wipe_on_loops_destination(paths, scale_(nozzle_diameter), is_ccw, is_hole);
|
||||
|
||||
bool wipe_inward_applied = false;
|
||||
// Orca: store loop paths in print order because inward offsets use this orientation.
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||
m_wipe.path = Polyline();
|
||||
for (ExtrusionPath &path : paths) {
|
||||
//BBS: Don't need to save duplicated point into wipe path
|
||||
if (!m_wipe.path.empty() && !path.empty() &&
|
||||
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
|
||||
// Convert Points3 to Points
|
||||
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
|
||||
m_wipe.path.append(Point(it->x(), it->y()));
|
||||
} else
|
||||
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
|
||||
m_wipe.update_path(paths);
|
||||
|
||||
// Orca: loop wipe paths retain print direction. Their material side is
|
||||
// therefore left for CCW contours and right for CW contours, with the
|
||||
// result inverted for holes. Only external perimeters are eligible.
|
||||
// Calibration overrides are applied during extrusion, after the region
|
||||
// context was created. Check the effective setting again at execution.
|
||||
if (m_config.wipe_inward && m_config.wipe_inward_distance.value > 0. &&
|
||||
wipe_support != nullptr && !wipe_support->inner_lines.empty() &&
|
||||
// A loop's role is its first path's role. An overhanging start must
|
||||
// not hide ordinary external-wall segments elsewhere in the loop.
|
||||
std::any_of(paths.begin(), paths.end(),
|
||||
[](const ExtrusionPath &path) { return is_external_perimeter(path.role()); }) &&
|
||||
m_wipe.path.points.size() >= 2) {
|
||||
// Orca: use the actual extrusion width from the path, not the config
|
||||
// value — outer_wall_line_width=0 (Auto) would make get_abs_value
|
||||
// return 0 and silently disable the feature, and Arachne may produce
|
||||
// a different width than the config default.
|
||||
const double outer_wall_line_width = paths.front().width;
|
||||
const double requested_offset = m_config.wipe_inward_distance.get_abs_value(outer_wall_line_width);
|
||||
const double offset_dist = scale_(std::min(requested_offset, outer_wall_line_width));
|
||||
if (offset_dist > SCALED_EPSILON) {
|
||||
const Point seam_start = paths.front().first_point();
|
||||
const Point seam_end = paths.back().last_point();
|
||||
const Point wipe_start = wipe_on_loops_dest.value_or(seam_end);
|
||||
const double max_wipe_length = scale_(FILAMENT_CONFIG(wipe_distance));
|
||||
// Orca: Wipe::wipe() replaces points[0] with last_pos and executes
|
||||
// from points[1]. The helper preserves that sentinel and atomically
|
||||
// replaces the remaining points, or leaves the path untouched.
|
||||
// Orca: a configured wall count does not guarantee that Arachne
|
||||
// generated an adjacent wall for this particular loop. Only
|
||||
// earlier entities are considered because later walls have
|
||||
// not been printed yet (for example with Outer/Inner order).
|
||||
// Inner walls determine the material side; every earlier wall
|
||||
// remains available to validate the executable wipe path.
|
||||
const double support_distance = scale_(std::max(nozzle_diameter, outer_wall_line_width));
|
||||
Polyline inward_path = m_wipe.path;
|
||||
if (offset_wipe_path_toward_support(
|
||||
inward_path, seam_start, seam_end, wipe_start,
|
||||
wipe_offset_direction(is_ccw, is_hole), offset_dist, max_wipe_length,
|
||||
wipe_support->inner_lines, wipe_support->printed_lines,
|
||||
m_wipe.path.lines(), support_distance)) {
|
||||
m_wipe.path = std::move(inward_path);
|
||||
wipe_inward_applied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make a little move inwards before leaving loop
|
||||
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter && m_layer != NULL && m_config.wall_loops.value > 1 && paths.front().size() >= 2 && paths.back().polyline.points.size() >= 3) {
|
||||
// detect angle between last and first segment
|
||||
// the side depends on the original winding order of the polygon (inwards for contours, outwards for holes)
|
||||
//FIXME improve the algorithm in case the loop is tiny.
|
||||
//FIXME improve the algorithm in case the loop is split into segments with a low number of points (see the Point b query).
|
||||
const Point3 &a3 = paths.front().polyline.points[1]; // second point
|
||||
Point a = Point(a3.x(), a3.y());
|
||||
const Point3 &b3 = *(paths.back().polyline.points.end()-3); // second to last point
|
||||
Point b = Point(b3.x(), b3.y());
|
||||
if (is_hole == loop.is_counter_clockwise()) {
|
||||
// swap points
|
||||
Point c = a; a = b; b = c;
|
||||
}
|
||||
|
||||
double angle = paths.front().first_point().ccw_angle(a, b) / 3;
|
||||
|
||||
// turn inwards if contour, turn outwards if hole
|
||||
if (is_hole == loop.is_counter_clockwise()) angle *= -1;
|
||||
|
||||
// create the destination point along the first segment and rotate it
|
||||
// we make sure we don't exceed the segment length because we don't know
|
||||
// the rotation of the second segment so we might cross the object boundary
|
||||
Vec2d p1 = paths.front().polyline.points.front().cast<double>().head<2>();
|
||||
Vec2d p2 = paths.front().polyline.points[1].cast<double>().head<2>();
|
||||
Vec2d v = p2 - p1;
|
||||
double nd = scale_(EXTRUDER_CONFIG(nozzle_diameter));
|
||||
double l2 = v.squaredNorm();
|
||||
// Shift by no more than a nozzle diameter.
|
||||
//FIXME Hiding the seams will not work nicely for very densely discretized contours!
|
||||
//BBS. shorten the travel distant before the wipe path
|
||||
double threshold = 0.2;
|
||||
Point pt = (p1 + v * threshold).cast<coord_t>();
|
||||
if (nd * nd < l2)
|
||||
pt = (p1 + threshold * v * (nd / sqrt(l2))).cast<coord_t>();
|
||||
//Point pt = ((nd * nd >= l2) ? (p1+v*0.4): (p1 + 0.2 * v * (nd / sqrt(l2)))).cast<coord_t>();
|
||||
const Point3 ¢er3 = paths.front().polyline.points.front();
|
||||
pt.rotate(angle, Point(center3.x(), center3.y()));
|
||||
// generate the travel move
|
||||
gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true);
|
||||
// Orca: make the configured inward move before leaving the loop.
|
||||
if (wipe_on_loops_dest) {
|
||||
gcode += m_writer.extrude_to_xy(
|
||||
this->point_to_gcode(*wipe_on_loops_dest), 0, "move inwards before travel", true);
|
||||
this->set_last_pos(*wipe_on_loops_dest);
|
||||
}
|
||||
|
||||
// Execute the accepted path before another extrusion replaces it. Wiping
|
||||
// must not force retraction or Z-hop across a short travel to the next wall.
|
||||
// Ordinary travel planning decides whether to retract from the new position.
|
||||
if (wipe_inward_applied)
|
||||
gcode += m_wipe.wipe(*this, 0.);
|
||||
|
||||
return gcode;
|
||||
}
|
||||
|
||||
@@ -7524,21 +7562,9 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
|
||||
m_multi_flow_segment_path_pa_set = true;
|
||||
}
|
||||
|
||||
// BBS
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||
m_wipe.path = Polyline();
|
||||
for (const ExtrusionPath &path : multipath.paths) {
|
||||
//BBS: Don't need to save duplicated point into wipe path
|
||||
if (!m_wipe.path.empty() && !path.empty() &&
|
||||
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
|
||||
// Convert Points3 to Points
|
||||
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
|
||||
m_wipe.path.append(Point(it->x(), it->y()));
|
||||
} else
|
||||
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
|
||||
}
|
||||
m_wipe.path.reverse();
|
||||
}
|
||||
// Orca: multipath wipes retrace the extrusion in reverse order.
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe))
|
||||
m_wipe.update_path(multipath.paths, true);
|
||||
|
||||
return gcode;
|
||||
}
|
||||
@@ -7546,14 +7572,15 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
|
||||
std::string GCode::extrude_entity(const ExtrusionEntity& entity,
|
||||
const std::string& description,
|
||||
double speed,
|
||||
const ExtrusionEntitiesPtr& region_perimeters)
|
||||
const ExtrusionEntitiesPtr& region_perimeters,
|
||||
const WipeInwardSupport* wipe_support)
|
||||
{
|
||||
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(&entity))
|
||||
return this->extrude_path(*path, description, speed);
|
||||
else if (const ExtrusionMultiPath* multipath = dynamic_cast<const ExtrusionMultiPath*>(&entity))
|
||||
return this->extrude_multi_path(*multipath, description, speed);
|
||||
else if (const ExtrusionLoop* loop = dynamic_cast<const ExtrusionLoop*>(&entity))
|
||||
return this->extrude_loop(*loop, description, speed, region_perimeters);
|
||||
return this->extrude_loop(*loop, description, speed, region_perimeters, nullptr, wipe_support);
|
||||
else
|
||||
throw Slic3r::InvalidArgument("Invalid argument supplied to extrude()");
|
||||
return "";
|
||||
@@ -7567,6 +7594,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
|
||||
// description += ExtrusionEntity::role_to_string(path.role());
|
||||
std::string gcode = this->_extrude(path, description, speed);
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
|
||||
m_wipe.reset_path();
|
||||
m_wipe.path = path.polyline.to_polyline();
|
||||
if (is_tree(this->config().support_type) && is_support(path.role())) {
|
||||
if ((m_wipe.path.first_point() - m_wipe.path.last_point()).cast<double>().norm() > scale_(0.2)) {
|
||||
@@ -7599,8 +7627,19 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
|
||||
: (m_config.is_infill_first == is_infill_first);
|
||||
if (!should_print) continue;
|
||||
|
||||
for (const ExtrusionEntity* ee : region.perimeters)
|
||||
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters);
|
||||
// Build the printed prefix once in emission order, scoped to this
|
||||
// region. Disabled or zero-length wipes need no support geometry.
|
||||
std::optional<WipeInwardSupport> wipe_support;
|
||||
if (m_wipe.enable && FILAMENT_CONFIG(wipe) && m_config.wipe_inward &&
|
||||
m_config.wipe_inward_distance.value > 0. &&
|
||||
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
|
||||
wipe_support.emplace();
|
||||
for (const ExtrusionEntity* ee : region.perimeters) {
|
||||
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
|
||||
wipe_support ? &*wipe_support : nullptr);
|
||||
if (wipe_support)
|
||||
wipe_support->append(*ee);
|
||||
}
|
||||
}
|
||||
return gcode;
|
||||
}
|
||||
@@ -7841,7 +7880,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
// path is 2D. But in slope lift case, lift z is done in travel_to function.
|
||||
// Add m_need_change_layer_lift_z when change_layer in case of no lift if m_last_pos is equal to path.first_point() by chance
|
||||
Point first_point = path.first_point();
|
||||
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z || slope_need_z_travel) {
|
||||
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z ||
|
||||
slope_need_z_travel) {
|
||||
const bool _last_pos_undefined = !m_last_pos_defined;
|
||||
|
||||
double z = DBL_MAX;
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Slic3r {
|
||||
|
||||
// Forward declarations.
|
||||
class GCode;
|
||||
struct WipeInwardSupport;
|
||||
|
||||
namespace CustomGCode{ struct Item; }
|
||||
struct PrintInstance;
|
||||
@@ -61,7 +62,7 @@ public:
|
||||
bool enable;
|
||||
Polyline path;
|
||||
|
||||
// Orca:
|
||||
// Orca: retraction portions emitted before, during, and after the wipe move.
|
||||
struct RetractionValues{
|
||||
double retraction_length_before_wipe = 0.;
|
||||
double retraction_length_during_wipe = 0.;
|
||||
@@ -73,8 +74,10 @@ public:
|
||||
void reset_path() { this->path = Polyline(); }
|
||||
std::string wipe(GCode &gcodegen, double length, bool toolchange = false, bool is_last = false);
|
||||
|
||||
// Orca:
|
||||
// Orca: calculate the retraction portions that can be emitted at wipe speed.
|
||||
RetractionValues calculateWipeRetractionLengths(GCode& gcodegen, bool toolchange);
|
||||
// Orca: rebuild the stored path while deduplicating shared path boundaries.
|
||||
void update_path(const ExtrusionPaths &paths, bool reverse = false);
|
||||
};
|
||||
|
||||
class WipeTowerIntegration {
|
||||
@@ -430,14 +433,16 @@ private:
|
||||
std::string extrude_entity(const ExtrusionEntity& entity,
|
||||
const std::string& description = "",
|
||||
double speed = -1.,
|
||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr());
|
||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
|
||||
const WipeInwardSupport* wipe_support = nullptr);
|
||||
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
|
||||
// should be executed
|
||||
std::string extrude_loop(const ExtrusionLoop& loop,
|
||||
const std::string& description,
|
||||
double speed = -1.,
|
||||
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
|
||||
const Point* start_point = nullptr);
|
||||
const Point* start_point = nullptr,
|
||||
const WipeInwardSupport* wipe_support = nullptr);
|
||||
std::string extrude_multi_path(const ExtrusionMultiPath& multipath, const std::string& description = "", double speed = -1.);
|
||||
std::string extrude_path(const ExtrusionPath& path, const std::string& description = "", double speed = -1.);
|
||||
|
||||
|
||||
@@ -0,0 +1,920 @@
|
||||
#include "WipePathHelpers.hpp"
|
||||
|
||||
#include "../AABBTreeLines.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <tuple>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void WipeInwardSupport::append(const ExtrusionEntity &entity)
|
||||
{
|
||||
const ExtrusionPaths *paths = nullptr;
|
||||
if (const auto *loop = dynamic_cast<const ExtrusionLoop *>(&entity))
|
||||
paths = &loop->paths;
|
||||
else if (const auto *multipath = dynamic_cast<const ExtrusionMultiPath *>(&entity))
|
||||
paths = &multipath->paths;
|
||||
|
||||
// A loop's role is its first path's role. An overhanging start must not
|
||||
// hide the ordinary inner-wall segments elsewhere in the same loop.
|
||||
const bool is_inner = paths ? std::any_of(paths->begin(), paths->end(),
|
||||
[](const ExtrusionPath &path) { return is_internal_perimeter(path.role()); }) :
|
||||
is_internal_perimeter(entity.role());
|
||||
const Lines lines = entity.as_polyline().lines();
|
||||
printed_lines.insert(printed_lines.end(), lines.begin(), lines.end());
|
||||
if (is_inner)
|
||||
inner_lines.insert(inner_lines.end(), lines.begin(), lines.end());
|
||||
}
|
||||
|
||||
// Orca: miter limit ratio. Matches DefaultMiterLimit from ClipperUtils.hpp.
|
||||
// When the miter join extends more than miter_limit * offset_dist from the
|
||||
// original vertex, the miter is replaced by a bevel join.
|
||||
static constexpr double miter_limit = 3.0;
|
||||
|
||||
// Orca: threshold for detecting near-reversal (backtracking spike).
|
||||
// Normalized dot product below this means the segments point in nearly
|
||||
// opposite directions (angle > ~172°). Offsetting such a path is unsafe.
|
||||
static constexpr double reversal_dot_threshold = -0.99;
|
||||
|
||||
// Orca: candidates pointing more than 60 degrees away from the selected inner
|
||||
// wall are too tangent to distinguish the material side reliably at a cusp.
|
||||
static constexpr double min_support_alignment = 0.5;
|
||||
|
||||
// Keep a scaled-coordinate rounding floor while allowing the tolerance to
|
||||
// follow the relevant offset or path length. Clearance allows a larger fraction.
|
||||
static double wipe_tolerance(double distance, double relative_tolerance = 0.1)
|
||||
{
|
||||
return std::max(4. * SCALED_EPSILON, relative_tolerance * distance);
|
||||
}
|
||||
|
||||
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target)
|
||||
{
|
||||
assert(!paths.empty());
|
||||
if (paths.empty())
|
||||
return Point(0, 0);
|
||||
|
||||
double remaining = target;
|
||||
Point result = forward ? paths.front().first_point() : paths.back().last_point();
|
||||
for (int pi = forward ? 0 : (int)paths.size() - 1;
|
||||
pi >= 0 && pi < (int)paths.size() && remaining > 0.;
|
||||
pi += forward ? 1 : -1) {
|
||||
const Points3 &pts = paths[pi].polyline.points;
|
||||
for (int i = forward ? 0 : (int)pts.size() - 1;
|
||||
remaining > 0. && (forward ? i + 1 < (int)pts.size() : i > 0);
|
||||
i += forward ? 1 : -1) {
|
||||
const int j = forward ? i + 1 : i - 1;
|
||||
const Point cur(pts[i].x(), pts[i].y());
|
||||
const Point next(pts[j].x(), pts[j].y());
|
||||
const double segment_length = (next - cur).cast<double>().norm();
|
||||
if (segment_length < SCALED_EPSILON)
|
||||
continue;
|
||||
if (remaining <= segment_length) {
|
||||
const double ratio = remaining / segment_length;
|
||||
return Point(coord_t(cur.x() + ratio * (next.x() - cur.x())),
|
||||
coord_t(cur.y() + ratio * (next.y() - cur.y())));
|
||||
}
|
||||
remaining -= segment_length;
|
||||
result = next;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Orca: consecutive duplicates carry no path length and can be removed safely.
|
||||
// A reversal, however, is real travelled distance: removing its vertex would
|
||||
// replace a long backtracking wipe with a short, unrelated shortcut.
|
||||
static bool prepare_source(Points &pts)
|
||||
{
|
||||
pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
|
||||
|
||||
if (pts.size() < 2)
|
||||
return false;
|
||||
|
||||
for (size_t i = 1; i + 1 < pts.size(); ++i) {
|
||||
const Vec2d v_prev = (pts[i] - pts[i - 1]).cast<double>();
|
||||
const Vec2d v_next = (pts[i + 1] - pts[i]).cast<double>();
|
||||
const double dot = v_prev.dot(v_next) / (v_prev.norm() * v_next.norm());
|
||||
if (dot < reversal_dot_threshold)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool build_offset_polyline(const Points &original, int dir, double offset_dist,
|
||||
Points &result, size_t &first_join_index)
|
||||
{
|
||||
if (original.size() < 2)
|
||||
return false;
|
||||
|
||||
// Orca: collapse all consecutive duplicates first, then reject any
|
||||
// backtracking in the cleaned path instead of replacing travelled distance
|
||||
// with a shortcut.
|
||||
Points source = original;
|
||||
if (! prepare_source(source))
|
||||
return false;
|
||||
|
||||
const size_t n = source.size();
|
||||
|
||||
// Orca: compute the perpendicular offset for segment i->i+1 as an infinite Line.
|
||||
auto offset_segment = [dir, offset_dist](const Point &a, const Point &b) -> Line {
|
||||
Vec2d v = (b - a).cast<double>();
|
||||
double len = v.norm();
|
||||
Vec2d perp(0, 0);
|
||||
if (len > SCALED_EPSILON)
|
||||
perp = Vec2d(-v.y(), v.x()) * (dir * offset_dist / len);
|
||||
return Line(Point(coord_t(a.x() + perp.x()), coord_t(a.y() + perp.y())),
|
||||
Point(coord_t(b.x() + perp.x()), coord_t(b.y() + perp.y())));
|
||||
};
|
||||
|
||||
result.clear();
|
||||
result.reserve(n);
|
||||
first_join_index = 0;
|
||||
|
||||
// Orca: the first point is perpendicular to the first segment.
|
||||
Line l_prev = offset_segment(source[0], source[1]);
|
||||
result.push_back(l_prev.a);
|
||||
|
||||
// Orca: use the analytic intersection of adjacent offset segments for a
|
||||
// miter join. Intersecting the already rounded Line endpoints amplifies
|
||||
// coordinate quantization when the source segments are nearly parallel.
|
||||
for (size_t i = 1; i + 1 < n; ++i) {
|
||||
Line l_next = offset_segment(source[i], source[i + 1]);
|
||||
const Vec2d previous = (source[i] - source[i - 1]).cast<double>().normalized();
|
||||
const Vec2d next = (source[i + 1] - source[i]).cast<double>().normalized();
|
||||
const double denominator = 1. + previous.dot(next);
|
||||
|
||||
bool need_bevel = denominator <= EPSILON;
|
||||
Point pt;
|
||||
if (! need_bevel) {
|
||||
const Vec2d previous_normal(-previous.y(), previous.x());
|
||||
const Vec2d next_normal(-next.y(), next.x());
|
||||
const Vec2d miter = (previous_normal + next_normal) * (dir * offset_dist / denominator);
|
||||
if (miter.norm() > miter_limit * offset_dist) {
|
||||
need_bevel = true;
|
||||
} else {
|
||||
pt = Point(coord_t(source[i].x() + miter.x()),
|
||||
coord_t(source[i].y() + miter.y()));
|
||||
}
|
||||
}
|
||||
|
||||
if (need_bevel) {
|
||||
result.push_back(l_prev.b);
|
||||
if (l_next.a != result.back())
|
||||
result.push_back(l_next.a);
|
||||
} else {
|
||||
result.push_back(pt);
|
||||
}
|
||||
if (i == 1)
|
||||
first_join_index = result.size() - 1;
|
||||
l_prev = l_next;
|
||||
}
|
||||
|
||||
// Orca: the last point is perpendicular to the last segment.
|
||||
result.push_back(l_prev.b);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int wipe_offset_direction(bool is_ccw, bool is_hole)
|
||||
{
|
||||
const int loop_inside = is_ccw ? +1 : -1;
|
||||
return is_hole ? -loop_inside : loop_inside;
|
||||
}
|
||||
|
||||
static bool starts_by_backtracking(const Polyline &path, Point actual_start)
|
||||
{
|
||||
if (path.points.size() < 3)
|
||||
return false;
|
||||
// Orca: points[0] is only a storage sentinel; use the nozzle position for
|
||||
// the executable connector, particularly after a wipe_on_loops pre-move.
|
||||
const Vec2d connector = (path.points[1] - actual_start).cast<double>();
|
||||
const Vec2d outgoing = (path.points[2] - path.points[1]).cast<double>();
|
||||
// An inward connector may be perpendicular to the outgoing offset edge.
|
||||
// Rounded joins must not turn that right angle into a false backtrack.
|
||||
return connector.dot(outgoing) < -4. * SCALED_EPSILON * outgoing.norm();
|
||||
}
|
||||
|
||||
// Orca: sample the outgoing perimeter without copying or clipping its full loop.
|
||||
static Point sample_polyline_at_distance(const Polyline &polyline, double target)
|
||||
{
|
||||
assert(! polyline.points.empty());
|
||||
Point result = polyline.first_point();
|
||||
for (size_t i = 1; i < polyline.points.size() && target > 0.; ++i) {
|
||||
const Vec2d segment = (polyline.points[i] - result).cast<double>();
|
||||
const double length = segment.norm();
|
||||
if (length <= SCALED_EPSILON)
|
||||
continue;
|
||||
if (target <= length)
|
||||
return (result.cast<double>() + segment * (target / length)).cast<coord_t>();
|
||||
target -= length;
|
||||
result = polyline.points[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Orca: convert an executable path into Wipe::wipe()'s stored representation.
|
||||
// The first point is a dummy replaced by the actual nozzle position, while the
|
||||
// remaining points are clipped to the configured wipe distance.
|
||||
static bool store_wipe_path(Polyline &destination, Point seam_start,
|
||||
Polyline actual_path, double max_wipe_length)
|
||||
{
|
||||
if (actual_path.points.size() < 2 || max_wipe_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const double actual_length = actual_path.length();
|
||||
if (actual_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
if (actual_length - max_wipe_length > SCALED_EPSILON)
|
||||
actual_path.clip_end(actual_length - max_wipe_length);
|
||||
if (actual_path.points.size() < 2)
|
||||
return false;
|
||||
for (size_t i = 1; i < actual_path.points.size(); ++i)
|
||||
if (actual_path.points[i - 1] == actual_path.points[i])
|
||||
return false;
|
||||
|
||||
Polyline stored_path;
|
||||
stored_path.points.reserve(actual_path.points.size());
|
||||
stored_path.points.push_back(seam_start);
|
||||
stored_path.points.insert(stored_path.points.end(), actual_path.points.begin() + 1, actual_path.points.end());
|
||||
stored_path.reset_to_linear_move();
|
||||
destination = std::move(stored_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int dir, double offset_dist, double max_wipe_length)
|
||||
{
|
||||
assert(dir == +1 || dir == -1);
|
||||
assert(offset_dist > 0);
|
||||
if (polyline.points.empty() || polyline.first_point() != seam_start ||
|
||||
max_wipe_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const Polyline original = polyline;
|
||||
const double original_length = original.length();
|
||||
if (original_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
double source_length = std::min(original_length, max_wipe_length);
|
||||
for (;;) {
|
||||
Polyline source = original;
|
||||
const double clip_distance = original_length - source_length;
|
||||
if (clip_distance > SCALED_EPSILON)
|
||||
source.clip_end(clip_distance);
|
||||
|
||||
Points wrapped_source;
|
||||
wrapped_source.reserve(source.points.size() + 1);
|
||||
if (seam_start == seam_end) {
|
||||
// Orca: the stored loop is open at seam_start even when the seam gap is
|
||||
// zero. Prepend the closing edge so build_offset_polyline() creates
|
||||
// the proper join between that edge and the first outgoing edge,
|
||||
// instead of leaving the first offset point on the closing wall.
|
||||
size_t closing_index = original.points.size();
|
||||
while (closing_index > 0 && original.points[closing_index - 1] == seam_start)
|
||||
--closing_index;
|
||||
if (closing_index == 0)
|
||||
return false; // Orca: the entire path is a single point.
|
||||
wrapped_source.push_back(original.points[closing_index - 1]);
|
||||
} else {
|
||||
// Orca: use the unextruded seam-gap edge to determine the incoming
|
||||
// direction at the seam. Its offset is construction geometry only;
|
||||
// wiping along it would create a Z-shaped detour before the outgoing
|
||||
// perimeter offset.
|
||||
wrapped_source.push_back(seam_end);
|
||||
}
|
||||
wrapped_source.insert(wrapped_source.end(), source.points.begin(), source.points.end());
|
||||
|
||||
Points offset_points;
|
||||
size_t first_join_index = 0;
|
||||
if (! build_offset_polyline(wrapped_source, dir, offset_dist, offset_points, first_join_index) ||
|
||||
first_join_index == 0 || first_join_index >= offset_points.size())
|
||||
return false;
|
||||
// Orca: discard the offset of the prepended edge and, for a bevel, its
|
||||
// incoming endpoint. The executable wipe starts at the seam join and
|
||||
// then follows only the already printed outgoing perimeter.
|
||||
offset_points.erase(offset_points.begin(), offset_points.begin() + first_join_index);
|
||||
|
||||
Polyline actual_path;
|
||||
actual_path.points.reserve(offset_points.size() + 1);
|
||||
actual_path.points.push_back(wipe_start);
|
||||
actual_path.points.insert(actual_path.points.end(), offset_points.begin(), offset_points.end());
|
||||
|
||||
// A loop pre-move may advance past an otherwise valid offset join.
|
||||
// Enter at the nozzle's projection instead of returning to the join.
|
||||
// Do not repair a join that already backtracks across the seam gap;
|
||||
// the caller must still validate wall crossings, material side and support.
|
||||
if (seam_start != seam_end && wipe_start != seam_start && wipe_start != seam_end &&
|
||||
starts_by_backtracking(actual_path, wipe_start) && ! starts_by_backtracking(actual_path, seam_end)) {
|
||||
size_t entry = 1;
|
||||
while (entry + 1 < actual_path.points.size()) {
|
||||
const Vec2d edge = (actual_path.points[entry + 1] - actual_path.points[entry]).cast<double>();
|
||||
const double projection = (wipe_start - actual_path.points[entry]).cast<double>().dot(edge);
|
||||
if (projection <= 0.)
|
||||
break;
|
||||
if (projection < edge.squaredNorm()) {
|
||||
actual_path.points[entry] = (actual_path.points[entry].cast<double>() +
|
||||
edge * (projection / edge.squaredNorm())).cast<coord_t>();
|
||||
break;
|
||||
}
|
||||
++entry;
|
||||
}
|
||||
actual_path.points.erase(actual_path.points.begin() + 1, actual_path.points.begin() + entry);
|
||||
}
|
||||
|
||||
if (seam_start != seam_end && wipe_start == seam_end &&
|
||||
starts_by_backtracking(actual_path, wipe_start)) {
|
||||
// Orca: a wide seam gap or a sharp cusp may put the first miter
|
||||
// behind its outgoing edge. Reject this offset candidate so the
|
||||
// caller can try the opposite side or the translated fallback.
|
||||
return false;
|
||||
}
|
||||
|
||||
const double actual_length = actual_path.length();
|
||||
const bool source_exhausted = original_length - source_length <= SCALED_EPSILON;
|
||||
if (actual_length + SCALED_EPSILON < max_wipe_length && ! source_exhausted) {
|
||||
// Orca: offset joins may shorten the path at every corner. Grow the
|
||||
// source until the executable offset path, not a heuristic source
|
||||
// margin, reaches the configured wipe distance.
|
||||
const double deficit = max_wipe_length - actual_length;
|
||||
const double next_length = std::min(original_length,
|
||||
source_length + std::max(deficit, 2. * SCALED_EPSILON));
|
||||
if (next_length - source_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
source_length = next_length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Orca: unlike an extruded offset, a wipe may safely cross or retrace the
|
||||
// just-printed perimeter. The caller validates the complete executable
|
||||
// path against current and earlier printed perimeter geometry.
|
||||
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
|
||||
}
|
||||
}
|
||||
|
||||
static bool translated_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
const Vec2d &translation, double max_wipe_length)
|
||||
{
|
||||
if (translation.norm() <= SCALED_EPSILON || max_wipe_length <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const Polyline original = polyline;
|
||||
Polyline actual_path;
|
||||
actual_path.points.reserve(original.points.size() + 2);
|
||||
actual_path.points.push_back(wipe_start);
|
||||
|
||||
const auto append_translated = [&actual_path, &translation](const Point &point) {
|
||||
const Point translated = (point.cast<double>() + translation).cast<coord_t>();
|
||||
if (translated != actual_path.points.back())
|
||||
actual_path.points.push_back(translated);
|
||||
};
|
||||
|
||||
// Orca: translate the seam join directly. Translating seam_end and then
|
||||
// following the unextruded gap back to seam_start makes the wipe double
|
||||
// back whenever a gap ends near a sharp corner.
|
||||
append_translated(seam_start);
|
||||
for (const Point &point : original.points)
|
||||
append_translated(point);
|
||||
|
||||
if (seam_start != seam_end && wipe_start == seam_end &&
|
||||
starts_by_backtracking(actual_path, wipe_start)) {
|
||||
// Orca: at a wide gap next to a cusp, the translated seam join may
|
||||
// lie behind the outgoing edge. Prefer a shorter local inward move
|
||||
// at the actual extrusion end over a longer lightning-shaped wipe.
|
||||
actual_path.points.resize(1);
|
||||
append_translated(seam_end);
|
||||
}
|
||||
|
||||
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
|
||||
}
|
||||
|
||||
// A segment whose endpoints lie within one line's distance capsule is fully
|
||||
// supported, since that capsule is convex. Subdivide only when support changes
|
||||
// between lines; fixed-distance sampling can miss an unsupported gap.
|
||||
static bool segment_is_supported(Point start, Point end,
|
||||
const AABBTreeLines::LinesDistancer<Line> &distancer,
|
||||
double max_distance)
|
||||
{
|
||||
const Point midpoint = ((start.cast<double>() + end.cast<double>()) * 0.5).cast<coord_t>();
|
||||
const auto [distance, line_index, nearest] = distancer.distance_from_lines_extra<false>(midpoint);
|
||||
if (distance > max_distance)
|
||||
return false;
|
||||
|
||||
const Line &line = distancer.get_line(line_index);
|
||||
if (line.distance_to(start) <= max_distance && line.distance_to(end) <= max_distance)
|
||||
return true;
|
||||
if (distancer.distance_from_lines<false>(start) > max_distance ||
|
||||
distancer.distance_from_lines<false>(end) > max_distance)
|
||||
return false;
|
||||
|
||||
// Conservatively reject an unresolved transition at coordinate precision.
|
||||
if ((end - start).cast<double>().norm() <= SCALED_EPSILON)
|
||||
return false;
|
||||
return segment_is_supported(start, midpoint, distancer, max_distance) &&
|
||||
segment_is_supported(midpoint, end, distancer, max_distance);
|
||||
}
|
||||
|
||||
std::optional<double> wipe_path_support_score(
|
||||
const Polyline &polyline, Point wipe_start,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
|
||||
double max_distance)
|
||||
{
|
||||
if (polyline.points.size() < 2 || target_distancer.get_lines().empty() || max_distance <= 0)
|
||||
return std::nullopt;
|
||||
|
||||
// Orca: require a local neighbour, not merely an earlier perimeter elsewhere in
|
||||
// the region. At a convex corner, an inner wall's miter is farther from the
|
||||
// external seam than its normal wall spacing, so allow the same bounded miter
|
||||
// reach as the offset construction without accepting a remote island.
|
||||
if (target_distancer.distance_from_lines<false>(wipe_start) >
|
||||
miter_limit * max_distance + 4. * SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
|
||||
Point previous = wipe_start;
|
||||
for (size_t i = 1; i < polyline.points.size(); ++i) {
|
||||
// Orca: a tightly curved inward path may cross back over the current wall.
|
||||
// This is safe for a non-extruding wipe as long as the complete path
|
||||
// remains over current or earlier printed perimeter geometry.
|
||||
// Allow the same coordinate-rounding tolerance at every point, including
|
||||
// the actual start substituted for the stored sentinel.
|
||||
if (! segment_is_supported(previous, polyline.points[i], all_support_distancer,
|
||||
max_distance + 4. * SCALED_EPSILON))
|
||||
return std::nullopt;
|
||||
previous = polyline.points[i];
|
||||
}
|
||||
|
||||
// Orca: decide direction at the seam. Scoring the complete path may select
|
||||
// the wrong initial side when two contours converge and the later prefix
|
||||
// happens to run closer to unrelated support.
|
||||
return target_distancer.distance_from_lines<false>(polyline.points[1]);
|
||||
}
|
||||
|
||||
static bool initial_connector_is_clear(
|
||||
const Polyline &polyline, Point wipe_start, Point seam_start,
|
||||
AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||
double contact_tolerance)
|
||||
{
|
||||
if (polyline.points.size() < 2 || polyline.points[1] == wipe_start)
|
||||
return false;
|
||||
|
||||
// Orca: without a seam gap, the connector necessarily starts at the wall
|
||||
// and a self-touching cusp may share that same endpoint on several edges.
|
||||
if (seam_start == wipe_start)
|
||||
return true;
|
||||
|
||||
const Line connector(wipe_start, polyline.points[1]);
|
||||
const auto intersections = current_perimeter_distancer.intersections_with_line<false>(connector);
|
||||
for (const auto &intersection : intersections) {
|
||||
if ((intersection.first - wipe_start).cast<double>().norm() > contact_tolerance)
|
||||
return false;
|
||||
}
|
||||
|
||||
Point closest;
|
||||
// Orca: integer offset joins may miss the exact seam-start coordinate by
|
||||
// a few microns. Treat a close pass through that point as retracing the
|
||||
// external wall, but keep the unavoidable contact at the actual start.
|
||||
if (connector.distance_to_squared(seam_start, &closest) <= contact_tolerance * contact_tolerance &&
|
||||
(closest - wipe_start).cast<double>().norm() > contact_tolerance)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::optional<Vec2d> support_offset_at_start(
|
||||
const Polyline &source, Point local_origin, bool disambiguate_branch,
|
||||
AABBTreeLines::LinesDistancer<Line> &support_distancer,
|
||||
double max_support_distance)
|
||||
{
|
||||
if (source.points.size() < 2)
|
||||
return std::nullopt;
|
||||
|
||||
// Orca: a nonzero gap may put the seam beside the wrong branch of a cusp.
|
||||
// Sample farther along the path to identify its actual neighbouring wall.
|
||||
const Point support_query = disambiguate_branch ?
|
||||
sample_polyline_at_distance(source, 2. * max_support_distance) : source.first_point();
|
||||
const auto nearest_result = support_distancer.distance_from_lines_extra<false>(support_query);
|
||||
const Line &nearest_line = support_distancer.get_line(std::get<1>(nearest_result));
|
||||
Vec2d sampled_offset = std::get<2>(nearest_result) - support_query.cast<double>();
|
||||
|
||||
if (disambiguate_branch) {
|
||||
// Orca: an endpoint projection also contains distance along the support
|
||||
// segment. Remove that tangent component before comparing wall sides.
|
||||
const Vec2d support_edge = (nearest_line.b - nearest_line.a).cast<double>();
|
||||
if (support_edge.norm() > SCALED_EPSILON) {
|
||||
const Vec2d support_tangent = support_edge.normalized();
|
||||
sampled_offset -= support_tangent * sampled_offset.dot(support_tangent);
|
||||
}
|
||||
}
|
||||
if (sampled_offset.norm() <= SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
|
||||
if (! disambiguate_branch)
|
||||
return sampled_offset;
|
||||
|
||||
// Orca: find the local point on the same material-side branch. Using the
|
||||
// sampled point itself would add the distance already travelled along the
|
||||
// perimeter and turn a normal transition into a long diagonal move.
|
||||
const Vec2d sampled_direction = sampled_offset.normalized();
|
||||
Vec2d local_offset = sampled_offset;
|
||||
double best_local_score = std::numeric_limits<double>::infinity();
|
||||
for (size_t line_index : support_distancer.all_lines_in_radius(
|
||||
local_origin, 2. * max_support_distance + 4. * SCALED_EPSILON)) {
|
||||
Point local_support;
|
||||
const Line &line = support_distancer.get_line(line_index);
|
||||
const double distance_squared = line.distance_to_squared(local_origin, &local_support);
|
||||
const Vec2d candidate_offset = local_support.cast<double>() - local_origin.cast<double>();
|
||||
const double candidate_distance = std::sqrt(distance_squared);
|
||||
if (candidate_distance <= SCALED_EPSILON)
|
||||
continue;
|
||||
const double alignment = candidate_offset.normalized().dot(sampled_direction);
|
||||
if (alignment < min_support_alignment)
|
||||
continue;
|
||||
const double score = candidate_distance / alignment;
|
||||
if (score < best_local_score) {
|
||||
best_local_score = score;
|
||||
local_offset = candidate_offset;
|
||||
}
|
||||
}
|
||||
return local_offset;
|
||||
}
|
||||
|
||||
static double executable_path_length(const Polyline &stored_path, Point wipe_start)
|
||||
{
|
||||
if (stored_path.points.size() < 2)
|
||||
return 0.;
|
||||
|
||||
// Orca: points[0] is the storage sentinel, so measure the first segment
|
||||
// from the actual nozzle position and the remaining stored segments normally.
|
||||
double length = (stored_path.points[1] - wipe_start).cast<double>().norm();
|
||||
for (size_t index = 2; index < stored_path.points.size(); ++index)
|
||||
length += (stored_path.points[index] - stored_path.points[index - 1]).cast<double>().norm();
|
||||
return length;
|
||||
}
|
||||
|
||||
static Lines material_side_support_lines(const Polyline &path, Point seam, int preferred_dir,
|
||||
const Lines &support_lines)
|
||||
{
|
||||
if (path.points.size() < 4 || path.first_point() != path.last_point())
|
||||
return {};
|
||||
|
||||
// Orca: the bisector of the incoming and outgoing material-side normals is
|
||||
// a local side test that remains valid for globally self-touching Arachne
|
||||
// contours. Ignore repeated seam points when obtaining both tangents.
|
||||
const auto outgoing_it = std::find_if(
|
||||
path.points.begin() + 1, path.points.end(), [seam](const Point &point) { return point != seam; });
|
||||
const auto incoming_it = std::find_if(
|
||||
path.points.rbegin() + 1, path.points.rend(), [seam](const Point &point) { return point != seam; });
|
||||
if (outgoing_it == path.points.end() || incoming_it == path.points.rend())
|
||||
return {};
|
||||
|
||||
const Vec2d outgoing = (*outgoing_it - seam).cast<double>().normalized();
|
||||
const Vec2d incoming = (seam - *incoming_it).cast<double>().normalized();
|
||||
const Vec2d material_direction =
|
||||
(Vec2d(-outgoing.y(), outgoing.x()) + Vec2d(-incoming.y(), incoming.x())) * preferred_dir;
|
||||
if (material_direction.norm() <= EPSILON)
|
||||
return {};
|
||||
|
||||
Lines result;
|
||||
result.reserve(support_lines.size());
|
||||
for (const Line &line : support_lines) {
|
||||
Point closest;
|
||||
line.distance_to_squared(seam, &closest);
|
||||
if ((closest - seam).cast<double>().dot(material_direction) > SCALED_EPSILON)
|
||||
result.push_back(line);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool wipe_path_stays_on_material_side(
|
||||
const Polyline &path, Point path_start, const Vec2d &support_direction,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||
double effective_offset, bool require_clearance)
|
||||
{
|
||||
if (path.points.size() < 2 || support_direction.norm() <= EPSILON ||
|
||||
target_perimeter_distancer.get_lines().empty() || current_perimeter_distancer.get_lines().empty() ||
|
||||
effective_offset <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
const Vec2d initial_offset = (path.points[1] - path_start).cast<double>();
|
||||
if (initial_offset.norm() <= SCALED_EPSILON ||
|
||||
initial_offset.normalized().dot(support_direction.normalized()) < min_support_alignment)
|
||||
return false;
|
||||
// Orca: after the connector has left the extrusion endpoint, an inward
|
||||
// offset must retain most of its requested clearance from the current
|
||||
// external wall. Otherwise a tight turn may send an initially correct path
|
||||
// back onto that wall, or make the opposite-side candidate look supported.
|
||||
const double clearance_tolerance = wipe_tolerance(effective_offset, 0.25);
|
||||
const double minimum_clearance = effective_offset - clearance_tolerance;
|
||||
const Lines &lines = current_perimeter_distancer.get_lines();
|
||||
const auto left_normal = [](const Line &line) -> Vec2d {
|
||||
const Vec2d edge = (line.b - line.a).cast<double>();
|
||||
if (edge.norm() <= SCALED_EPSILON)
|
||||
return Vec2d::Zero();
|
||||
return Vec2d(-edge.y(), edge.x()).normalized();
|
||||
};
|
||||
const auto on_material_side = [&](const Point &point, bool check_clearance) {
|
||||
const auto [distance, line_index, nearest] =
|
||||
current_perimeter_distancer.distance_from_lines_extra<false>(point);
|
||||
if (line_index >= lines.size())
|
||||
return false;
|
||||
const Line &line = lines[line_index];
|
||||
Vec2d normal = left_normal(line);
|
||||
// At a shared vertex use both incident edges, so the result does not
|
||||
// depend on which equally close edge the AABB query happens to return.
|
||||
const Line &previous = lines[(line_index + lines.size() - 1) % lines.size()];
|
||||
const Line &next = lines[(line_index + 1) % lines.size()];
|
||||
if ((nearest - line.a.cast<double>()).norm() <= SCALED_EPSILON && previous.b == line.a)
|
||||
normal += left_normal(previous);
|
||||
if ((nearest - line.b.cast<double>()).norm() <= SCALED_EPSILON && next.a == line.b)
|
||||
normal += left_normal(next);
|
||||
if (normal.norm() <= EPSILON)
|
||||
return false;
|
||||
|
||||
// An open or self-touching wall has no reliable polygon-wide sign.
|
||||
// Orient its local normal toward the neighbouring printed inner wall,
|
||||
// then test the candidate on that side at every sample.
|
||||
normal.normalize();
|
||||
const Point wall_point = nearest.cast<coord_t>();
|
||||
const Vec2d support_point = std::get<2>(
|
||||
target_perimeter_distancer.distance_from_lines_extra<false>(wall_point));
|
||||
const double support_side = (support_point - nearest).dot(normal);
|
||||
if (std::abs(support_side) <= 4. * SCALED_EPSILON)
|
||||
return false;
|
||||
const double side = (point.cast<double>() - nearest).dot(normal) * (support_side > 0. ? 1. : -1.);
|
||||
return side >= -4. * SCALED_EPSILON &&
|
||||
(! check_clearance || distance + 4. * SCALED_EPSILON >= minimum_clearance);
|
||||
};
|
||||
|
||||
Point previous = path.points[1];
|
||||
if (! on_material_side(previous, require_clearance))
|
||||
return false;
|
||||
for (size_t index = 2; index < path.points.size(); ++index) {
|
||||
const Vec2d segment = (path.points[index] - previous).cast<double>();
|
||||
const size_t samples = std::max<size_t>(1, size_t(std::ceil(segment.norm() / effective_offset)));
|
||||
for (size_t sample = 1; sample <= samples; ++sample) {
|
||||
const Point point = (previous.cast<double>() +
|
||||
segment * (double(sample) / double(samples))).cast<coord_t>();
|
||||
if (! on_material_side(point, require_clearance))
|
||||
return false;
|
||||
}
|
||||
previous = path.points[index];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int preferred_dir, double offset_dist, double max_wipe_length,
|
||||
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
|
||||
const Lines ¤t_perimeter_lines,
|
||||
double max_support_distance)
|
||||
{
|
||||
assert(preferred_dir == +1 || preferred_dir == -1);
|
||||
if (polyline.points.size() < 2 || target_perimeter_lines.empty() || current_perimeter_lines.empty() ||
|
||||
offset_dist <= SCALED_EPSILON ||
|
||||
max_wipe_length <= SCALED_EPSILON || max_support_distance <= SCALED_EPSILON)
|
||||
return false;
|
||||
|
||||
Lines material_support_lines;
|
||||
const Lines *candidate_support_lines = &target_perimeter_lines;
|
||||
if (seam_start == seam_end) {
|
||||
// Orca: another contour may have a geometrically closer inner wall on
|
||||
// this loop's air side. Restrict zero-gap support using the local seam
|
||||
// normals before choosing the nearest wall.
|
||||
material_support_lines = material_side_support_lines(
|
||||
polyline, seam_start, preferred_dir, target_perimeter_lines);
|
||||
if (material_support_lines.empty())
|
||||
return false;
|
||||
candidate_support_lines = &material_support_lines;
|
||||
}
|
||||
|
||||
AABBTreeLines::LinesDistancer<Line> support_distancer(*candidate_support_lines);
|
||||
const std::optional<Vec2d> support_offset = support_offset_at_start(
|
||||
polyline, seam_end, seam_start != seam_end,
|
||||
support_distancer, max_support_distance);
|
||||
if (! support_offset)
|
||||
return false;
|
||||
const Vec2d toward_support = *support_offset;
|
||||
const double local_support_distance = toward_support.norm();
|
||||
const double effective_offset = std::min(offset_dist, local_support_distance);
|
||||
if (effective_offset <= SCALED_EPSILON)
|
||||
return false;
|
||||
const Vec2d support_direction = toward_support / local_support_distance;
|
||||
|
||||
// Orca: every candidate is validated against the same generated geometry.
|
||||
// Build these AABB trees once per loop instead of rebuilding them for each
|
||||
// preferred, alternate, translated, direct, or reversed candidate.
|
||||
Lines all_support_lines = printed_perimeter_lines;
|
||||
all_support_lines.insert(all_support_lines.end(), current_perimeter_lines.begin(), current_perimeter_lines.end());
|
||||
AABBTreeLines::LinesDistancer<Line> all_support_distancer(std::move(all_support_lines));
|
||||
AABBTreeLines::LinesDistancer<Line> current_perimeter_distancer(current_perimeter_lines);
|
||||
|
||||
// Orca: allow only the contact needed to leave the extrusion endpoint. A
|
||||
// connector that meets the current wall again is a seam-gap retrace, even
|
||||
// if the rest of the non-extruding wipe remains over printed material.
|
||||
const double contact_tolerance = wipe_tolerance(effective_offset);
|
||||
|
||||
struct Candidate {
|
||||
Polyline path;
|
||||
// Orca: support score chooses the material-side path; length is used
|
||||
// only to replace a corner-truncated path with the reverse fallback.
|
||||
double support_score;
|
||||
double path_length;
|
||||
};
|
||||
|
||||
// Direction and wall contact have different origins after a loop pre-move.
|
||||
// Keep the construction's wall endpoint for intersection checks even when
|
||||
// the candidate's direction must be checked from the current nozzle position.
|
||||
const auto validate_candidate = [&](Polyline path, Point path_start, Point direction_start,
|
||||
double path_contact_tolerance,
|
||||
const Vec2d &candidate_support_direction,
|
||||
double candidate_offset,
|
||||
bool require_clearance = true) -> std::optional<Candidate> {
|
||||
// Orca: backtracking indicates a wrong join only across a nonzero gap.
|
||||
// A closed zero-gap offset may initially turn back at its miter while
|
||||
// still remaining on the supported material side of the perimeter.
|
||||
const bool backtracks_across_gap = seam_start != seam_end && starts_by_backtracking(path, wipe_start);
|
||||
// At a clipped corner another branch of the current wall may be closer
|
||||
// than the requested offset. Preserve the zero-gap clearance rule, but
|
||||
// check direction and local material side independently for every gap.
|
||||
const bool material_side = wipe_path_stays_on_material_side(
|
||||
path, direction_start, candidate_support_direction,
|
||||
support_distancer, current_perimeter_distancer, candidate_offset,
|
||||
require_clearance && seam_start == seam_end);
|
||||
const bool connector_clear = initial_connector_is_clear(
|
||||
path, wipe_start, path_start, current_perimeter_distancer, path_contact_tolerance);
|
||||
if (backtracks_across_gap || ! material_side || ! connector_clear)
|
||||
return std::nullopt;
|
||||
const std::optional<double> score = wipe_path_support_score(
|
||||
path, wipe_start, support_distancer, all_support_distancer, max_support_distance);
|
||||
if (! score)
|
||||
return std::nullopt;
|
||||
const double path_length = executable_path_length(path, wipe_start);
|
||||
return Candidate{std::move(path), *score, path_length};
|
||||
};
|
||||
|
||||
const auto offset_candidate = [&](int dir) -> std::optional<Candidate> {
|
||||
Polyline path = polyline;
|
||||
if (! offset_wipe_path(path, seam_start, seam_end, wipe_start, dir,
|
||||
effective_offset, max_wipe_length))
|
||||
return std::nullopt;
|
||||
return validate_candidate(std::move(path), seam_start, seam_start,
|
||||
contact_tolerance, support_direction, effective_offset);
|
||||
};
|
||||
|
||||
std::optional<Candidate> preferred = offset_candidate(preferred_dir);
|
||||
std::optional<Candidate> alternate = offset_candidate(-preferred_dir);
|
||||
|
||||
// Orca: forward and reverse fallbacks share the same clamping, translation,
|
||||
// connector tolerance, and complete-path validation.
|
||||
const auto translated_candidate = [&](Polyline source, Point source_start, Point source_end,
|
||||
const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
|
||||
const double support_distance = candidate_support_offset.norm();
|
||||
const double candidate_offset = std::min(offset_dist, support_distance);
|
||||
if (candidate_offset <= SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
|
||||
const Vec2d candidate_translation = candidate_support_offset * (candidate_offset / support_distance);
|
||||
if (! translated_wipe_path(source, source_start, source_end, wipe_start,
|
||||
candidate_translation, max_wipe_length))
|
||||
return std::nullopt;
|
||||
const double candidate_tolerance = wipe_tolerance(candidate_offset);
|
||||
return validate_candidate(std::move(source), source_start, source_start, candidate_tolerance,
|
||||
candidate_support_offset / support_distance, candidate_offset);
|
||||
};
|
||||
|
||||
std::optional<Candidate> translated = translated_candidate(polyline, seam_start, seam_end, toward_support);
|
||||
|
||||
// Orca: if every full-length construction folds back onto the external
|
||||
// wall, retain a short direct inward move instead of accepting an outward
|
||||
// candidate or falling back to the standard wipe along the outer wall.
|
||||
const auto direct_candidate = [&](Point origin, const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
|
||||
const double support_distance = candidate_support_offset.norm();
|
||||
const double candidate_offset = std::min(offset_dist, support_distance);
|
||||
if (candidate_offset <= SCALED_EPSILON)
|
||||
return std::nullopt;
|
||||
const Vec2d direction = candidate_support_offset / support_distance;
|
||||
const Point destination = (origin.cast<double>() + direction * candidate_offset).cast<coord_t>();
|
||||
if (destination == wipe_start)
|
||||
return std::nullopt;
|
||||
|
||||
Polyline path;
|
||||
if (! store_wipe_path(path, seam_start, Polyline{wipe_start, destination}, max_wipe_length))
|
||||
return std::nullopt;
|
||||
const double candidate_tolerance = wipe_tolerance(candidate_offset);
|
||||
// Check the executed direction from the nozzle after any loop pre-move,
|
||||
// but retain the wall origin for the connector's intersection checks.
|
||||
return validate_candidate(std::move(path), origin, wipe_start,
|
||||
candidate_tolerance, direction, candidate_offset, false);
|
||||
};
|
||||
std::optional<Candidate> direct = direct_candidate(seam_end, toward_support);
|
||||
|
||||
const double length_margin = wipe_tolerance(max_wipe_length);
|
||||
std::optional<Candidate> reversed;
|
||||
if (seam_start != seam_end && polyline.last_point() == seam_end) {
|
||||
// Orca: when a large gap straddles a sharp corner, connecting the
|
||||
// extrusion end to the forward offset may either reverse or leave only
|
||||
// a short local move. The already printed incoming wall is equally safe:
|
||||
// follow it backwards and determine its own material-side support.
|
||||
Polyline reversed_source = polyline;
|
||||
reversed_source.reverse();
|
||||
const std::optional<Vec2d> reversed_support_offset = support_offset_at_start(
|
||||
reversed_source, seam_end, true, support_distancer, max_support_distance);
|
||||
if (reversed_support_offset) {
|
||||
reversed = translated_candidate(reversed_source, seam_end, seam_end, *reversed_support_offset);
|
||||
// A translated reverse path can backtrack or leave the material on
|
||||
// a curved wall. Offset the incoming wall itself when translation
|
||||
// cannot supply a complete wipe, retaining all candidate checks.
|
||||
if (! reversed || reversed->path_length + length_margin < max_wipe_length) {
|
||||
const double reverse_offset = std::min(offset_dist, reversed_support_offset->norm());
|
||||
if (reverse_offset > SCALED_EPSILON &&
|
||||
offset_wipe_path(reversed_source, seam_end, seam_start, wipe_start,
|
||||
-preferred_dir, reverse_offset, max_wipe_length)) {
|
||||
reversed_source.points.front() = seam_start;
|
||||
auto candidate = validate_candidate(std::move(reversed_source), seam_end, seam_end,
|
||||
wipe_tolerance(reverse_offset), reversed_support_offset->normalized(), reverse_offset);
|
||||
if (candidate && (! reversed ||
|
||||
(candidate->path_length > reversed->path_length + length_margin &&
|
||||
candidate->support_score <= reversed->support_score + wipe_tolerance(reverse_offset))))
|
||||
reversed = std::move(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: conventional offsets at a narrow cusp may form a bevel across the
|
||||
// cusp. Candidates pointing away from the actual inner wall are rejected
|
||||
// during validation; among the remaining paths, prefer the one whose first
|
||||
// point is materially closer to that wall.
|
||||
const double direction_change_margin = wipe_tolerance(effective_offset);
|
||||
std::optional<Candidate> selected = std::move(preferred);
|
||||
if (translated) {
|
||||
if (! selected || translated->support_score + direction_change_margin < selected->support_score)
|
||||
selected = std::move(translated);
|
||||
}
|
||||
if (! selected)
|
||||
selected = std::move(direct);
|
||||
// Prefer a direct inward move when the normal offset cannot be used.
|
||||
// An alternate offset is eligible only after the same material-side checks.
|
||||
if (! selected)
|
||||
selected = std::move(alternate);
|
||||
|
||||
// Orca: prefer a complete reverse wipe over a forward fallback that had to
|
||||
// stop at the corner. Equal-length paths keep the normal forward behavior.
|
||||
if (reversed && (! selected ||
|
||||
(reversed->path_length > selected->path_length + length_margin &&
|
||||
reversed->support_score <= selected->support_score + direction_change_margin)))
|
||||
selected = std::move(reversed);
|
||||
if (! selected)
|
||||
return false;
|
||||
|
||||
polyline = std::move(selected->path);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
|
||||
bool is_ccw, bool is_hole)
|
||||
{
|
||||
assert(!paths.empty());
|
||||
assert(nozzle_diam_scaled > 0);
|
||||
if (paths.empty() || nozzle_diam_scaled <= 0)
|
||||
return std::nullopt;
|
||||
|
||||
// Orca: clamp sample distance to L/4 so forward/backward samples cannot meet.
|
||||
double total_length = 0.;
|
||||
for (const ExtrusionPath &path : paths)
|
||||
total_length += path.length();
|
||||
const double sample_distance = std::min(nozzle_diam_scaled, total_length * 0.25);
|
||||
|
||||
Point a = sample_path_at_distance(paths, true, sample_distance);
|
||||
Point b = sample_path_at_distance(paths, false, sample_distance);
|
||||
|
||||
const Point seam_start = paths.front().first_point();
|
||||
|
||||
// Orca: skip the inward move for degenerate geometry.
|
||||
if (a == b || a == seam_start || b == seam_start)
|
||||
return std::nullopt;
|
||||
|
||||
const bool reverse_turn = is_hole == is_ccw;
|
||||
if (reverse_turn)
|
||||
std::swap(a, b);
|
||||
|
||||
double angle = seam_start.ccw_angle(a, b) / 3;
|
||||
|
||||
// Orca: reject degenerate angles near 0 or 2π.
|
||||
static constexpr double angle_epsilon = 0.01;
|
||||
if (angle < angle_epsilon || angle > 2 * PI / 3 - angle_epsilon)
|
||||
return std::nullopt;
|
||||
|
||||
if (reverse_turn)
|
||||
angle *= -1;
|
||||
|
||||
Point pt = sample_path_at_distance(paths, true, std::min(0.2 * nozzle_diam_scaled, sample_distance));
|
||||
pt.rotate(angle, seam_start);
|
||||
return pt;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "../ExtrusionEntity.hpp"
|
||||
#include "../Polyline.hpp"
|
||||
#include "../Line.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Printed prefix of one region's perimeter sequence. Append each entity only
|
||||
// after extrusion; later walls and other regions cannot support an inward wipe.
|
||||
struct WipeInwardSupport {
|
||||
Lines printed_lines;
|
||||
Lines inner_lines;
|
||||
void append(const ExtrusionEntity &entity);
|
||||
};
|
||||
|
||||
namespace AABBTreeLines {
|
||||
template <typename LineType> class LinesDistancer;
|
||||
}
|
||||
|
||||
// Orca: sample a point at a given distance along ExtrusionPaths, walking
|
||||
// across segment boundaries. forward=true walks from paths.front, false from
|
||||
// paths.back. For tiny loops the walk stops early and returns the last
|
||||
// reachable point. Returns the start point if target is zero.
|
||||
// Precondition: paths must be non-empty.
|
||||
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target);
|
||||
|
||||
// Orca: return the side of the printed path on which the material lies.
|
||||
// dir +1 is left and -1 is right, matching the offset-builder convention.
|
||||
int wipe_offset_direction(bool is_ccw, bool is_hole);
|
||||
|
||||
// Orca: atomically offset a stored wipe path. The seam-gap or closing edge
|
||||
// determines the join with the first outgoing perimeter edge, but its offset
|
||||
// is not part of the executable wipe. Only the prefix needed by Wipe::wipe()
|
||||
// is offset. Returns false and leaves polyline unchanged if that path cannot
|
||||
// be constructed without degenerate segments. This only constructs a candidate;
|
||||
// offset_wipe_path_toward_support() validates its support, material side and
|
||||
// connector before accepting it. The first stored point
|
||||
// remains a dummy preserving Wipe::wipe()'s convention of skipping points[0].
|
||||
// Precondition: polyline starts at seam_start, dir is +1 or -1, and
|
||||
// offset_dist > 0. A non-positive max_wipe_length returns false.
|
||||
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int dir, double offset_dist, double max_wipe_length);
|
||||
|
||||
// Orca: score a candidate's first destination by distance to the target inner
|
||||
// walls. Return nullopt if no target wall is near wipe_start or any executable
|
||||
// segment lacks support. target_distancer contains eligible earlier walls;
|
||||
// all_support_distancer includes the current wall and all earlier walls.
|
||||
// The stored first point is a dummy: the first segment starts at wipe_start.
|
||||
// This checks support only; material-side and connector checks belong to
|
||||
// offset_wipe_path_toward_support(). Trees are reused across its candidates.
|
||||
std::optional<double> wipe_path_support_score(
|
||||
const Polyline &polyline, Point wipe_start,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
|
||||
double max_distance);
|
||||
|
||||
// Validate the initial inward direction and the local material side along the
|
||||
// executable path, using the inner wall to orient the open current wall's
|
||||
// normals. Clearance is optional for clipped corners and short direct fallbacks;
|
||||
// the material-side check is mandatory. The straight connector is checked by
|
||||
// its initial direction and separately by support and intersection validation.
|
||||
// path_start is the construction origin; points[0] is only a storage sentinel.
|
||||
bool wipe_path_stays_on_material_side(
|
||||
const Polyline &path, Point path_start, const Vec2d &support_direction,
|
||||
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
|
||||
const AABBTreeLines::LinesDistancer<Line> ¤t_perimeter_distancer,
|
||||
double effective_offset, bool require_clearance);
|
||||
|
||||
// Orca: identify the adjacent inner perimeter from the outgoing wall, excluding
|
||||
// support on the air side of a closed zero-gap loop. Clamp the requested offset
|
||||
// to the distance from the seam end to that support, then select the safest
|
||||
// supported offset or translated path. If a wide seam gap at a corner truncates
|
||||
// every forward candidate, the incoming printed wall may be followed backwards
|
||||
// instead. All earlier printed perimeters still participate in the complete-path
|
||||
// safety check. This handles converging, locally ambiguous, or self-touching
|
||||
// contours whose global winding alone does not identify the material side.
|
||||
// Returns false and leaves polyline unchanged when no candidate is supported.
|
||||
// Precondition: preferred_dir is +1 or -1. Distances must be positive.
|
||||
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
|
||||
int preferred_dir, double offset_dist, double max_wipe_length,
|
||||
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
|
||||
const Lines ¤t_perimeter_lines,
|
||||
double max_support_distance);
|
||||
|
||||
// Orca: compute the inward destination point for wipe_on_loops, or
|
||||
// std::nullopt when the geometry is degenerate (tiny loop, coincident samples,
|
||||
// angle near 0 or 2π). Returns the rotated destination or nullopt to skip the
|
||||
// inward move entirely.
|
||||
// Precondition: paths non-empty, nozzle_diam_scaled > 0.
|
||||
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
|
||||
bool is_ccw, bool is_hole);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "LayOnFace.hpp"
|
||||
|
||||
#include "Geometry.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &inst_matrix)
|
||||
{
|
||||
// An object can only rest on its convex hull, so candidate faces are taken from the hull of all model parts.
|
||||
TriangleMesh ch;
|
||||
for (const ModelVolume* vol : object.volumes) {
|
||||
if (vol->type() != ModelVolumeType::MODEL_PART)
|
||||
continue;
|
||||
TriangleMesh vol_ch = vol->get_convex_hull();
|
||||
vol_ch.transform(vol->get_matrix());
|
||||
ch.merge(vol_ch);
|
||||
}
|
||||
ch = ch.convex_hull_3d();
|
||||
std::vector<LayOnFacePlane> planes;
|
||||
|
||||
// Following constants are used for discarding too small polygons.
|
||||
const float minimal_area = 5.f; // in square mm (world coordinates)
|
||||
const float minimal_side = 1.f; // mm
|
||||
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
|
||||
|
||||
// Now we'll go through all the facets and append Points of facets sharing the same normal.
|
||||
// This part is still performed in mesh coordinate system.
|
||||
const int num_of_facets = ch.facets_count();
|
||||
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
|
||||
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
|
||||
std::vector<int> facet_queue(num_of_facets, 0);
|
||||
std::vector<bool> facet_visited(num_of_facets, false);
|
||||
int facet_queue_cnt = 0;
|
||||
const stl_normal* normal_ptr = nullptr;
|
||||
int facet_idx = 0;
|
||||
while (1) {
|
||||
// Find next unvisited triangle:
|
||||
for (; facet_idx < num_of_facets; ++ facet_idx)
|
||||
if (!facet_visited[facet_idx]) {
|
||||
facet_queue[facet_queue_cnt ++] = facet_idx;
|
||||
facet_visited[facet_idx] = true;
|
||||
normal_ptr = &face_normals[facet_idx];
|
||||
planes.emplace_back();
|
||||
break;
|
||||
}
|
||||
if (facet_idx == num_of_facets)
|
||||
break; // Everything was visited already
|
||||
|
||||
while (facet_queue_cnt > 0) {
|
||||
int facet_idx = facet_queue[-- facet_queue_cnt];
|
||||
const stl_normal& this_normal = face_normals[facet_idx];
|
||||
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
|
||||
const Vec3i32 face = ch.its.indices[facet_idx];
|
||||
for (int j=0; j<3; ++j)
|
||||
planes.back().outline.emplace_back(ch.its.vertices[face[j]].cast<double>());
|
||||
|
||||
facet_visited[facet_idx] = true;
|
||||
for (int j = 0; j < 3; ++ j)
|
||||
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
|
||||
facet_queue[facet_queue_cnt ++] = neighbor_idx;
|
||||
}
|
||||
}
|
||||
planes.back().normal = normal_ptr->cast<double>();
|
||||
|
||||
Pointf3s& verts = planes.back().outline;
|
||||
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
|
||||
// make real sense.
|
||||
verts = transform(verts, inst_matrix);
|
||||
|
||||
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
|
||||
if (verts.size() == 3 &&
|
||||
((verts[0] - verts[1]).norm() < minimal_side
|
||||
|| (verts[0] - verts[2]).norm() < minimal_side
|
||||
|| (verts[1] - verts[2]).norm() < minimal_side))
|
||||
planes.pop_back();
|
||||
}
|
||||
|
||||
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
|
||||
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
|
||||
// Now we'll go through all the polygons, transform the points into xy plane to process them:
|
||||
for (unsigned int polygon_id=0; polygon_id < planes.size(); ++polygon_id) {
|
||||
Pointf3s& polygon = planes[polygon_id].outline;
|
||||
const Vec3d& normal = planes[polygon_id].normal;
|
||||
|
||||
// transform the normal according to the instance matrix:
|
||||
const Vec3d normal_transformed = normal_matrix * normal;
|
||||
|
||||
// We are going to rotate about z and y to flatten the plane
|
||||
Eigen::Quaterniond q;
|
||||
Transform3d& m = planes[polygon_id].to_plane_frame;
|
||||
m = Transform3d::Identity();
|
||||
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
|
||||
polygon = transform(polygon, m);
|
||||
|
||||
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
|
||||
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
|
||||
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
|
||||
Vec3d bb_size = BoundingBoxf3(polygon).size();
|
||||
float sf = std::min(1./bb_size(0), 1./bb_size(1));
|
||||
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
|
||||
polygon = transform(polygon, tr);
|
||||
polygon = Slic3r::Geometry::convex_hull(polygon);
|
||||
polygon = transform(polygon, tr.inverse());
|
||||
|
||||
// Calculate area of the polygons and discard ones that are too small
|
||||
float& area = planes[polygon_id].area;
|
||||
area = 0.f;
|
||||
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
|
||||
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
|
||||
area = 0.5f * std::abs(area);
|
||||
|
||||
bool discard = false;
|
||||
if (area < minimal_area)
|
||||
discard = true;
|
||||
else {
|
||||
// We also check the inner angles and discard polygons with angles smaller than the following threshold
|
||||
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
|
||||
|
||||
for (unsigned int i = 0; i < polygon.size(); ++i) {
|
||||
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
|
||||
const Vec3d& curr = polygon[i];
|
||||
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
|
||||
|
||||
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
|
||||
discard = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discard) {
|
||||
planes[polygon_id--] = std::move(planes.back());
|
||||
planes.pop_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
const Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0)) / double(polygon.size());
|
||||
planes[polygon_id].center = inst_matrix.inverse() * (m.inverse() * centroid);
|
||||
}
|
||||
|
||||
std::sort(planes.rbegin(), planes.rend(), [](const LayOnFacePlane& a, const LayOnFacePlane& b) { return a.area < b.area; });
|
||||
return planes;
|
||||
}
|
||||
|
||||
int find_largest_plane(const std::vector<LayOnFacePlane> &planes)
|
||||
{
|
||||
// The plane frame maps the instance normal to +Z, so the normal's z in instance coordinates is element (2, 2).
|
||||
auto downward = [](const LayOnFacePlane &plane) { return -plane.to_plane_frame.linear()(2, 2); };
|
||||
// Areas are floats from rounded geometry, so faces within 0.1% count as equal.
|
||||
int best = -1;
|
||||
for (size_t i = 0; i < planes.size() && planes[i].area >= planes.front().area * (1. - 1e-3); ++i)
|
||||
if (best < 0 || downward(planes[i]) > downward(planes[best]))
|
||||
best = int(i);
|
||||
return best;
|
||||
}
|
||||
|
||||
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction)
|
||||
{
|
||||
const Vec3d dir = direction.normalized();
|
||||
int best = -1;
|
||||
double best_dot = -2.;
|
||||
for (size_t i = 0; i < planes.size(); ++i)
|
||||
if (const double dot = planes[i].normal.dot(dir); dot > best_dot) {
|
||||
best_dot = dot;
|
||||
best = int(i);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
|
||||
const Vec3d &point, double tolerance)
|
||||
{
|
||||
const Vec3d instance_point = instance_matrix_no_offset * point;
|
||||
for (size_t i = 0; i < planes.size(); ++i) {
|
||||
const Pointf3s &outline = planes[i].outline;
|
||||
if (outline.empty())
|
||||
continue;
|
||||
const Vec3d p = planes[i].to_plane_frame * instance_point;
|
||||
// Facets with slightly different normals are merged into one face, so the outline is not exactly flat.
|
||||
const double z = std::accumulate(outline.begin(), outline.end(), 0., [](double sum, const Vec3d &v) { return sum + v.z(); }) / double(outline.size());
|
||||
if (std::abs(p.z() - z) > tolerance)
|
||||
continue;
|
||||
// The outline is convex: the point is inside when it is not on both sides of its edges.
|
||||
bool left = false, right = false;
|
||||
for (size_t j = 0; j < outline.size(); ++j) {
|
||||
const Vec2d a = outline[j].head<2>();
|
||||
const Vec2d edge = outline[(j + 1) % outline.size()].head<2>() - a;
|
||||
const double len = edge.norm();
|
||||
if (len < EPSILON)
|
||||
continue;
|
||||
const double side = cross2(edge, Vec2d(p.head<2>() - a)) / len;
|
||||
left |= side > tolerance;
|
||||
right |= side < -tolerance;
|
||||
}
|
||||
if (!(left && right))
|
||||
return int(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal)
|
||||
{
|
||||
ModelInstance &instance = *object.instances[instance_idx];
|
||||
const Geometry::Transformation &trafo = instance.get_transformation();
|
||||
// Same rotation as Selection::flattening_rotate(): turn the transformed normal to point down.
|
||||
const Vec3d tnormal = trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
|
||||
const Transform3d rotation = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
|
||||
instance.set_transformation(Geometry::Transformation(trafo.get_offset_matrix() * rotation * trafo.get_matrix_no_offset()));
|
||||
// Drop this instance only: ensure_on_bed() skips instances without auto_drop and measures the first instance.
|
||||
object.translate_instance(instance_idx, -object.instance_bounding_box(instance_idx).min.z() * Vec3d::UnitZ());
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "Point.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ModelObject;
|
||||
|
||||
// A face of an object's convex hull that the object can rest on. These are the faces the
|
||||
// "Lay on Face" gizmo offers and the ones the CLI --ground-* options choose from.
|
||||
//
|
||||
// Frames: "object" coordinates have the volume transformations applied but not the instance
|
||||
// transformation. "Instance" coordinates additionally have the instance rotation, scale and
|
||||
// mirror applied, but not its offset.
|
||||
struct LayOnFacePlane
|
||||
{
|
||||
Vec3d normal; // outward unit normal, object coordinates
|
||||
Vec3d center; // centroid of the outline, object coordinates; on the face's mean plane
|
||||
float area; // mm², instance coordinates
|
||||
Pointf3s outline; // convex outline in the plane frame, where the face is horizontal
|
||||
Transform3d to_plane_frame; // rotation from instance coordinates to the plane frame
|
||||
};
|
||||
|
||||
// Candidate faces of the object's model parts, largest first. The instance transformation
|
||||
// (without offset) is applied before measuring, so faces too small to rest on are dropped
|
||||
// by their printed size: under 5 mm², a side under 1 mm, or an inner angle under 1°.
|
||||
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &instance_matrix_no_offset);
|
||||
|
||||
// Index of the largest plane, or -1 if `planes` is empty. Of planes with the same area, such as
|
||||
// the top and bottom of a box, the one already facing down the most wins, so flat parts stay put.
|
||||
int find_largest_plane(const std::vector<LayOnFacePlane> &planes);
|
||||
|
||||
// Index of the plane whose normal is closest to `direction` (object coordinates),
|
||||
// or -1 if `planes` is empty.
|
||||
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction);
|
||||
|
||||
// Index of the plane whose face contains `point` (object coordinates) within `tolerance` mm, or -1
|
||||
// if there is none. `instance_matrix_no_offset` is the one the planes were computed with.
|
||||
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
|
||||
const Vec3d &point, double tolerance);
|
||||
|
||||
// Rotates the instance so that `normal` (object coordinates) points down, the same rotation as
|
||||
// the gizmo applies, then drops the instance so its lowest point is at z = 0.
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -545,7 +545,7 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str
|
||||
return "";
|
||||
|
||||
// Dedicated namespace for preset setting_ids, distinct from the cloud per-user
|
||||
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_id_tool.py;
|
||||
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_profile_tool.py;
|
||||
// never change this constant.
|
||||
static const boost::uuids::uuid vendor_namespace =
|
||||
boost::uuids::string_generator()("c1f4d9e2-7a3b-5c8d-9e0f-1a2b3c4d5e6f");
|
||||
@@ -1282,6 +1282,8 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"accel_to_decel_enable",
|
||||
"accel_to_decel_factor",
|
||||
"wipe_on_loops",
|
||||
"wipe_inward",
|
||||
"wipe_inward_distance",
|
||||
"wipe_before_external_loop",
|
||||
"bridge_density",
|
||||
"internal_bridge_density",
|
||||
|
||||
@@ -93,8 +93,8 @@ class PresetBundle;
|
||||
|
||||
// Deterministic preset setting_id: uuid5(vendor/type/name) -> 16 base62 chars.
|
||||
// Pure function of a system preset's identity, so the value can be assigned by
|
||||
// scripts/orca_id_tool.py and recomputed here when a profile ships without it.
|
||||
// MUST stay byte-identical to scripts/orca_id_tool.py.
|
||||
// scripts/orca_profile_tool.py and recomputed here when a profile ships without it.
|
||||
// MUST stay byte-identical to scripts/orca_profile_tool.py.
|
||||
// This is NOT the per-user cloud-sync setting_id
|
||||
// (OrcaCloudServiceAgent::generate_uuid_for_setting_id) - do not conflate them.
|
||||
std::string generate_preset_setting_id(const std::string& vendor,
|
||||
|
||||
@@ -6782,7 +6782,7 @@ std::string PresetBundle::load_vendor_preset(
|
||||
loaded.description = entry.description;
|
||||
loaded.setting_id = entry.setting_id;
|
||||
// Derive the preset setting_id on the fly when a profile ships without one,
|
||||
// matching scripts/orca_id_tool.py. Only instantiated presets carry an id;
|
||||
// matching scripts/orca_profile_tool.py. Only instantiated presets carry an id;
|
||||
// non-instantiated base profiles return earlier above. This never
|
||||
// touches the per-user cloud-sync setting_id written into user .info files.
|
||||
if (loaded.setting_id.empty() && entry.instantiation == "true")
|
||||
|
||||
@@ -233,6 +233,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
"accel_to_decel_enable",
|
||||
"accel_to_decel_factor",
|
||||
"wipe_on_loops",
|
||||
"wipe_inward",
|
||||
"wipe_inward_distance",
|
||||
"gcode_comments",
|
||||
"gcode_label_objects",
|
||||
"exclude_object",
|
||||
|
||||
@@ -6267,6 +6267,35 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("wipe_inward", coBool);
|
||||
def->label = L("Wipe inward");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Applies only to external walls, including hole boundaries. Moves the hot nozzle toward printed "
|
||||
"inner walls during wiping to reduce reheating of freshly printed plastic and seam marks.\n\n"
|
||||
"Especially useful at layer heights below 0.1 mm, where wipe marks are more visible.\n\n"
|
||||
"Uses the regular wipe if no adjacent inner wall is already printed (single-wall areas or "
|
||||
"Outer/Inner wall order), or if no supported inward path can be found, for example at tight "
|
||||
"corners or seam gaps.");
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("wipe_inward_distance", coFloatOrPercent);
|
||||
def->label = L("Wipe inward distance");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("The distance the wipe path is shifted away from the external perimeter, specified in millimeters "
|
||||
"or as a percentage of the actual outer-wall extrusion width.\n\n"
|
||||
"For example, 50% shifts the path by half of the outer-wall width. The effective offset is limited "
|
||||
"by both the actual outer-wall width and the available spacing to the adjacent wall, so values "
|
||||
"above 100% or an equivalent absolute distance have no additional effect. "
|
||||
"Set to 0 to disable the offset.");
|
||||
def->sidetext = L("mm or %");
|
||||
def->ratio_over = "outer_wall_line_width";
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->max_literal = 2; // Orca: G-code generation also clamps literal values to the actual outer-wall width.
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionFloatOrPercent(50, true));
|
||||
|
||||
def = this->add("wipe_before_external_loop", coBool);
|
||||
def->label = L("Wipe before external loop");
|
||||
def->category = L("Quality");
|
||||
@@ -11894,6 +11923,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def->tooltip = L("Do not run any validity checks, such as G-code path conflicts check.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// --strict turns the non-critical slicing warnings the CLI otherwise only logs into a
|
||||
// failed run, and records strict_mode in result.json so consumers can tell the modes apart.
|
||||
def = this->add("strict", coBool);
|
||||
def->label = L("Strict mode");
|
||||
def->tooltip = L("Exit non-zero when slicing raises a non-critical warning that is "
|
||||
"otherwise only logged, such as a model that needs support while "
|
||||
"support is disabled. Use this in CI or scripted pipelines that should "
|
||||
"never ship a subtly broken slice. Each such warning is also listed "
|
||||
"with a stable class in the `warnings` array of result.json, which is "
|
||||
"written on Linux only. Cannot be combined with --no-check, which skips "
|
||||
"the support check.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("normative_check", coBool);
|
||||
def->label = L("Normative check");
|
||||
def->tooltip = L("Check the normative items.");
|
||||
@@ -11914,6 +11956,13 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def->tooltip = L("This outputs the model\u2019s information.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("inspect_mesh", coBool);
|
||||
def->label = L("Inspect mesh (JSON to stdout)");
|
||||
def->tooltip = L("Print a JSON summary of each loaded object to stdout, then exit: its bounding boxes and the "
|
||||
"convex hull faces it can be laid on, with their normals, areas and centers. These are the faces "
|
||||
"the --ground-* options choose from. Machine-readable alternative to --info.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("export_settings", coString);
|
||||
def->label = L("Export Settings");
|
||||
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
|
||||
@@ -12033,6 +12082,34 @@ CLITransformConfigDef::CLITransformConfigDef()
|
||||
def->sidetext = u8"°"; // degrees, don't need translation
|
||||
def->set_default_value(new ConfigOptionFloat(0));
|
||||
|
||||
// The --ground-* options choose from the faces the "Lay on Face" gizmo offers. Like the other
|
||||
// transforms they run in command-line order, so they see the rotations given before them.
|
||||
def = this->add("ground_largest_face", coBool);
|
||||
def->label = L("Ground largest face");
|
||||
def->tooltip = L("Lay each object on the largest face of its convex hull and drop it onto the bed. Of equally large "
|
||||
"faces, the one already facing down is kept. Objects without a face large enough to rest on are left "
|
||||
"as they are. Transforms run in command-line order, so rotations given before this option are respected. "
|
||||
"--orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("ground_face_normal", coString);
|
||||
def->label = L("Ground face by normal");
|
||||
def->tooltip = L("Lay each object on the convex hull face whose outward normal is closest to the direction NX,NY,NZ "
|
||||
"and drop it onto the bed. The direction is in object coordinates, which include the rotations given "
|
||||
"before this option and match the plate axes unless the input file rotates the object. For example, "
|
||||
"1,0,0 stands the object on its +X side. --orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->cli_params = "NX,NY,NZ";
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("ground_face_point", coString);
|
||||
def->label = L("Ground face at point");
|
||||
def->tooltip = L("Lay each object on the convex hull face that contains the point X,Y,Z and drop it onto the bed. "
|
||||
"The point is in object coordinates, which include the rotations given before this option; "
|
||||
"--inspect-mesh reports face centers in them. Objects without such a face are left as they are, and "
|
||||
"the run fails if no object has one. --orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->cli_params = "X,Y,Z";
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("scale", coFloat);
|
||||
def->label = L("Scale");
|
||||
def->tooltip = L("Scale the model by a float factor.");
|
||||
|
||||
@@ -1391,6 +1391,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, role_based_wipe_speed))
|
||||
((ConfigOptionFloatOrPercent, wipe_speed))
|
||||
((ConfigOptionBool, wipe_on_loops))
|
||||
((ConfigOptionBool, wipe_inward))
|
||||
((ConfigOptionFloatOrPercent, wipe_inward_distance))
|
||||
((ConfigOptionBool, wipe_before_external_loop))
|
||||
((ConfigOptionEnum<WallInfillOrder>, wall_infill_order))
|
||||
((ConfigOptionBool, precise_outer_wall))
|
||||
|
||||
@@ -1574,6 +1574,8 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "brim_flow_ratio"
|
||||
|| opt_key == "filament_flow_ratio"
|
||||
|| opt_key == "scarf_joint_flow_ratio"
|
||||
|| opt_key == "wipe_inward"
|
||||
|| opt_key == "wipe_inward_distance"
|
||||
|| opt_key == "spiral_starting_flow_ratio"
|
||||
|| opt_key == "spiral_finishing_flow_ratio") {
|
||||
invalidated |= m_print->invalidate_step(psGCodeExport);
|
||||
|
||||
@@ -685,6 +685,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/bambu_networking.hpp
|
||||
Utils/Bonjour.cpp
|
||||
Utils/Bonjour.hpp
|
||||
Utils/MeshInspect.cpp
|
||||
Utils/MeshInspect.hpp
|
||||
Utils/CalibUtils.cpp
|
||||
Utils/CalibUtils.hpp
|
||||
Utils/ColorSpaceConvert.cpp
|
||||
|
||||
@@ -1104,6 +1104,9 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
auto is_role_based_wipe_speed = config->opt_bool("role_based_wipe_speed");
|
||||
toggle_field("wipe_speed",!is_role_based_wipe_speed);
|
||||
|
||||
const bool have_wipe_inward = config->opt_bool("wipe_inward");
|
||||
toggle_line("wipe_inward_distance", have_wipe_inward);
|
||||
|
||||
for (auto el : {"accel_to_decel_enable", "accel_to_decel_factor"})
|
||||
toggle_line(el, gcf_is_klipper);
|
||||
if(gcf_is_klipper)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
|
||||
|
||||
#include "libslic3r/Geometry/ConvexHull.hpp"
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
#include <numeric>
|
||||
@@ -45,10 +45,10 @@ void GLGizmoFlatten::data_changed(bool is_serializing)
|
||||
const ModelObject *model_object = nullptr;
|
||||
int instance_id = -1;
|
||||
if (selection.is_single_full_instance() ||
|
||||
selection.is_from_single_object() ) {
|
||||
selection.is_from_single_object() ) {
|
||||
model_object = selection.get_model()->objects[selection.get_object_idx()];
|
||||
instance_id = selection.get_instance_idx();
|
||||
}
|
||||
}
|
||||
set_flattening_data(model_object, instance_id);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ void GLGizmoFlatten::on_render()
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
|
||||
shader->start_using();
|
||||
glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
|
||||
|
||||
@@ -152,134 +152,18 @@ void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object, int in
|
||||
void GLGizmoFlatten::update_planes()
|
||||
{
|
||||
const ModelObject* mo = m_c->selection_info()->model_object();
|
||||
TriangleMesh ch;
|
||||
for (const ModelVolume* vol : mo->volumes) {
|
||||
if (vol->type() != ModelVolumeType::MODEL_PART)
|
||||
continue;
|
||||
TriangleMesh vol_ch = vol->get_convex_hull();
|
||||
vol_ch.transform(vol->get_matrix());
|
||||
ch.merge(vol_ch);
|
||||
}
|
||||
ch = ch.convex_hull_3d();
|
||||
const Transform3d &inst_matrix = mo->instances.front()->get_matrix_no_offset();
|
||||
// The candidate faces are shared with the CLI --ground-* options, the rest only prepares them for rendering.
|
||||
std::vector<LayOnFacePlane> planes = lay_on_face_planes(*mo, inst_matrix);
|
||||
m_planes.clear();
|
||||
on_unregister_raycasters_for_picking();
|
||||
const Transform3d &inst_matrix = mo->instances.front()->get_matrix_no_offset();
|
||||
|
||||
// Following constants are used for discarding too small polygons.
|
||||
const float minimal_area = 5.f; // in square mm (world coordinates)
|
||||
const float minimal_side = 1.f; // mm
|
||||
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
|
||||
// We only keep the 254 largest planes (because of the picking pass limitations):
|
||||
planes.resize(std::min((int)planes.size(), 254));
|
||||
|
||||
// Now we'll go through all the facets and append Points of facets sharing the same normal.
|
||||
// This part is still performed in mesh coordinate system.
|
||||
const int num_of_facets = ch.facets_count();
|
||||
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
|
||||
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
|
||||
std::vector<int> facet_queue(num_of_facets, 0);
|
||||
std::vector<bool> facet_visited(num_of_facets, false);
|
||||
int facet_queue_cnt = 0;
|
||||
const stl_normal* normal_ptr = nullptr;
|
||||
int facet_idx = 0;
|
||||
while (1) {
|
||||
// Find next unvisited triangle:
|
||||
for (; facet_idx < num_of_facets; ++ facet_idx)
|
||||
if (!facet_visited[facet_idx]) {
|
||||
facet_queue[facet_queue_cnt ++] = facet_idx;
|
||||
facet_visited[facet_idx] = true;
|
||||
normal_ptr = &face_normals[facet_idx];
|
||||
m_planes.emplace_back();
|
||||
break;
|
||||
}
|
||||
if (facet_idx == num_of_facets)
|
||||
break; // Everything was visited already
|
||||
|
||||
while (facet_queue_cnt > 0) {
|
||||
int facet_idx = facet_queue[-- facet_queue_cnt];
|
||||
const stl_normal& this_normal = face_normals[facet_idx];
|
||||
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
|
||||
const Vec3i32 face = ch.its.indices[facet_idx];
|
||||
for (int j=0; j<3; ++j)
|
||||
m_planes.back().vertices.emplace_back(ch.its.vertices[face[j]].cast<double>());
|
||||
|
||||
facet_visited[facet_idx] = true;
|
||||
for (int j = 0; j < 3; ++ j)
|
||||
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
|
||||
facet_queue[facet_queue_cnt ++] = neighbor_idx;
|
||||
}
|
||||
}
|
||||
m_planes.back().normal = normal_ptr->cast<double>();
|
||||
|
||||
Pointf3s& verts = m_planes.back().vertices;
|
||||
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
|
||||
// make real sense.
|
||||
verts = transform(verts, inst_matrix);
|
||||
|
||||
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
|
||||
if (verts.size() == 3 &&
|
||||
((verts[0] - verts[1]).norm() < minimal_side
|
||||
|| (verts[0] - verts[2]).norm() < minimal_side
|
||||
|| (verts[1] - verts[2]).norm() < minimal_side))
|
||||
m_planes.pop_back();
|
||||
}
|
||||
|
||||
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
|
||||
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
|
||||
// Now we'll go through all the polygons, transform the points into xy plane to process them:
|
||||
for (unsigned int polygon_id=0; polygon_id < m_planes.size(); ++polygon_id) {
|
||||
Pointf3s& polygon = m_planes[polygon_id].vertices;
|
||||
const Vec3d& normal = m_planes[polygon_id].normal;
|
||||
|
||||
// transform the normal according to the instance matrix:
|
||||
const Vec3d normal_transformed = normal_matrix * normal;
|
||||
|
||||
// We are going to rotate about z and y to flatten the plane
|
||||
Eigen::Quaterniond q;
|
||||
Transform3d m = Transform3d::Identity();
|
||||
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
|
||||
polygon = transform(polygon, m);
|
||||
|
||||
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
|
||||
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
|
||||
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
|
||||
Vec3d bb_size = BoundingBoxf3(polygon).size();
|
||||
float sf = std::min(1./bb_size(0), 1./bb_size(1));
|
||||
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
|
||||
polygon = transform(polygon, tr);
|
||||
polygon = Slic3r::Geometry::convex_hull(polygon);
|
||||
polygon = transform(polygon, tr.inverse());
|
||||
|
||||
// Calculate area of the polygons and discard ones that are too small
|
||||
float& area = m_planes[polygon_id].area;
|
||||
area = 0.f;
|
||||
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
|
||||
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
|
||||
area = 0.5f * std::abs(area);
|
||||
|
||||
bool discard = false;
|
||||
if (area < minimal_area)
|
||||
discard = true;
|
||||
else {
|
||||
// We also check the inner angles and discard polygons with angles smaller than the following threshold
|
||||
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
|
||||
|
||||
for (unsigned int i = 0; i < polygon.size(); ++i) {
|
||||
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
|
||||
const Vec3d& curr = polygon[i];
|
||||
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
|
||||
|
||||
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
|
||||
discard = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discard) {
|
||||
m_planes[polygon_id--] = std::move(m_planes.back());
|
||||
m_planes.pop_back();
|
||||
continue;
|
||||
}
|
||||
for (LayOnFacePlane& plane : planes) {
|
||||
// The outline is convex and lies in the plane frame, where the plane is horizontal.
|
||||
Pointf3s& polygon = plane.outline;
|
||||
|
||||
// We will shrink the polygon a little bit so it does not touch the object edges:
|
||||
Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0));
|
||||
@@ -332,13 +216,12 @@ void GLGizmoFlatten::update_planes()
|
||||
b(2) += 0.1f;
|
||||
|
||||
// Transform back to 3D (and also back to mesh coordinates)
|
||||
polygon = transform(polygon, inst_matrix.inverse() * m.inverse());
|
||||
m_planes.emplace_back();
|
||||
m_planes.back().normal = plane.normal;
|
||||
m_planes.back().area = plane.area;
|
||||
m_planes.back().vertices = transform(polygon, inst_matrix.inverse() * plane.to_plane_frame.inverse());
|
||||
}
|
||||
|
||||
// We'll sort the planes by area and only keep the 254 largest ones (because of the picking pass limitations):
|
||||
std::sort(m_planes.rbegin(), m_planes.rend(), [](const PlaneData& a, const PlaneData& b) { return a.area < b.area; });
|
||||
m_planes.resize(std::min((int)m_planes.size(), 254));
|
||||
|
||||
// Planes are finished - let's save what we calculated it from:
|
||||
m_volumes_matrices.clear();
|
||||
m_volumes_types.clear();
|
||||
|
||||
@@ -15815,6 +15815,7 @@ void Plater::calib_pa(const Calib_Params& params)
|
||||
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
|
||||
print_config->set_key_value("overhang_reverse", new ConfigOptionBool(false));
|
||||
print_config->set_key_value("precise_z_height", new ConfigOptionBool(false));
|
||||
print_config->set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
|
||||
switch (params.mode) {
|
||||
case CalibMode::Calib_PA_Line:
|
||||
@@ -16500,6 +16501,7 @@ void Plater::calib_retraction(const Calib_Params& params)
|
||||
auto obj = model().objects[0];
|
||||
|
||||
print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
|
||||
print_config->set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||
|
||||
float nozzle_diameter = printer_config->option<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
|
||||
float layer_height;
|
||||
|
||||
@@ -2669,6 +2669,8 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("role_based_wipe_speed","quality_settings_seam#role-based-wipe-speed");
|
||||
optgroup->append_single_option_line("wipe_speed", "quality_settings_seam#wipe-speed");
|
||||
optgroup->append_single_option_line("wipe_on_loops","quality_settings_seam#wipe-on-loop-inward-movement");
|
||||
optgroup->append_single_option_line("wipe_inward", "quality_settings_seam#wipe-inward");
|
||||
optgroup->append_single_option_line("wipe_inward_distance", "quality_settings_seam#wipe-inward");
|
||||
optgroup->append_single_option_line("wipe_before_external_loop","quality_settings_seam#wipe-before-external");
|
||||
|
||||
|
||||
|
||||
@@ -1096,6 +1096,7 @@ bool CalibUtils::calib_generic_PA(const CalibInfo &calib_info, wxString &error_m
|
||||
calib_pa_pattern(calib_info, model);
|
||||
|
||||
DynamicPrintConfig print_config = calib_info.print_prest->config;
|
||||
print_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||
DynamicPrintConfig filament_config = calib_info.filament_prest->config;
|
||||
DynamicPrintConfig printer_config = calib_info.printer_prest->config;
|
||||
|
||||
@@ -1357,6 +1358,7 @@ void CalibUtils::calib_retraction(const CalibInfo &calib_info, wxString &error_m
|
||||
read_model_from_file(input_file, model);
|
||||
|
||||
DynamicPrintConfig print_config = calib_info.print_prest->config;
|
||||
print_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
|
||||
DynamicPrintConfig filament_config = calib_info.filament_prest->config;
|
||||
DynamicPrintConfig printer_config = calib_info.printer_prest->config;
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "MeshInspect.hpp"
|
||||
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ostream>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace MeshInspect {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
static json to_json(const Vec3d &v) { return json::array({ v.x(), v.y(), v.z() }); }
|
||||
|
||||
static json to_json(const BoundingBoxf3 &bb)
|
||||
{
|
||||
return { { "min", to_json(bb.min) }, { "max", to_json(bb.max) }, { "size", to_json(bb.size()) } };
|
||||
}
|
||||
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths, std::ostream &out, size_t max_planes)
|
||||
{
|
||||
json objects = json::array();
|
||||
for (const ModelObject *mo : model.objects) {
|
||||
json obj = { { "name", mo->name },
|
||||
{ "triangle_count", mo->facets_count() },
|
||||
{ "instance_count", mo->instances.size() },
|
||||
{ "bbox_object", to_json(mo->raw_mesh_bounding_box()) } };
|
||||
if (!mo->instances.empty()) {
|
||||
const std::vector<LayOnFacePlane> planes = lay_on_face_planes(*mo, mo->instances.front()->get_matrix_no_offset());
|
||||
json planes_json = json::array();
|
||||
for (size_t i = 0; i < std::min(planes.size(), max_planes); ++i)
|
||||
planes_json.push_back({ { "normal", to_json(planes[i].normal) },
|
||||
{ "area_mm2", std::round(double(planes[i].area) * 1000.) / 1000. },
|
||||
{ "center", to_json(planes[i].center) } });
|
||||
obj["bbox_world"] = to_json(mo->instance_bounding_box(0));
|
||||
obj["instance_offset"] = to_json(mo->instances.front()->get_offset());
|
||||
obj["plane_count"] = planes.size();
|
||||
obj["planes"] = std::move(planes_json);
|
||||
}
|
||||
objects.push_back(std::move(obj));
|
||||
}
|
||||
|
||||
const json root = {
|
||||
{ "sources", source_paths },
|
||||
{ "note", "Lengths in mm. bbox_object and the plane normals and centers are in object coordinates: the parts as "
|
||||
"currently transformed, without the instance transformation. --ground-face-normal and "
|
||||
"--ground-face-point take values in these coordinates. area_mm2 uses instance 0's scale, "
|
||||
"bbox_world is instance 0 on the plate." },
|
||||
{ "objects", std::move(objects) },
|
||||
};
|
||||
// Object names and file paths are arbitrary bytes, and dump() throws on invalid UTF-8 by default.
|
||||
// Replace such sequences with U+FFFD so the output is always valid JSON.
|
||||
out << root.dump(2, ' ', false, json::error_handler_t::replace) << std::endl;
|
||||
}
|
||||
|
||||
} // namespace MeshInspect
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class Model;
|
||||
|
||||
namespace MeshInspect {
|
||||
|
||||
// Writes the --inspect-mesh JSON for `model` to `out`: per object its bounding boxes and the faces
|
||||
// it can be laid on, taken from lay_on_face_planes() so they are the faces the --ground-* options
|
||||
// choose from, in the frame those options take. At most `max_planes` faces are listed per object,
|
||||
// largest first. `source_paths` lists every input file; the CLI merges them into one model.
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths, std::ostream &out, size_t max_planes = 8);
|
||||
|
||||
} // namespace MeshInspect
|
||||
} // namespace Slic3r
|
||||
Reference in New Issue
Block a user