mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-07 15:51:08 +00:00
Merge branch 'main' into feature/texture_displacement
This commit is contained in:
@@ -54,12 +54,12 @@ public:
|
||||
int & i,
|
||||
Eigen::Matrix<double, 1, 3> &closest)
|
||||
{
|
||||
size_t idx_unsigned = 0;
|
||||
Vec3d closest_vec3d(closest);
|
||||
double dist =
|
||||
size_t idx_unsigned { 0 };
|
||||
Vec3d closest_vec3d { Vec3d::Zero() };
|
||||
const double dist {
|
||||
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
|
||||
its.vertices, its.indices, m_tree, point, idx_unsigned,
|
||||
closest_vec3d);
|
||||
closest_vec3d) };
|
||||
i = int(idx_unsigned);
|
||||
closest = closest_vec3d;
|
||||
return dist;
|
||||
@@ -311,10 +311,9 @@ AABBMesh::hit_result IndexedMesh::filter_hits(
|
||||
|
||||
|
||||
double AABBMesh::squared_distance(const Vec3d &p, int& i, Vec3d& c) const {
|
||||
double sqdst = 0;
|
||||
Eigen::Matrix<double, 1, 3> pp = p;
|
||||
Eigen::Matrix<double, 1, 3> cc;
|
||||
sqdst = m_aabb->squared_distance(*m_tm, pp, i, cc);
|
||||
const Eigen::Matrix<double, 1, 3> pp { p };
|
||||
Eigen::Matrix<double, 1, 3> cc { Vec3d::Zero() };
|
||||
const double sqdst { m_aabb->squared_distance(*m_tm, pp, i, cc) };
|
||||
c = cc;
|
||||
return sqdst;
|
||||
}
|
||||
|
||||
@@ -31,8 +31,9 @@ namespace AABBTreeLines {
|
||||
inline VectorType closest_point_to_origin(size_t primitive_index, ScalarType& squared_distance) const
|
||||
{
|
||||
Vec<LineType::Dim, typename LineType::Scalar> nearest_point;
|
||||
Vec<LineType::Dim, typename LineType::Scalar> cast_origin = origin.template cast<typename LineType::Scalar>();
|
||||
const LineType& line = lines[primitive_index];
|
||||
squared_distance = line_alg::distance_to_squared(line, origin.template cast<typename LineType::Scalar>(), &nearest_point);
|
||||
squared_distance = line_alg::distance_to_squared(line, cast_origin, &nearest_point);
|
||||
return nearest_point.template cast<ScalarType>();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -202,10 +202,25 @@ void AppConfig::set_defaults()
|
||||
if (get("seq_top_layer_only").empty())
|
||||
set("seq_top_layer_only", "1");
|
||||
|
||||
// ORCA: darken layers below the current one while scrubbing the preview (ported from preFlight)
|
||||
// ORCA: darken the layers the preview layer slider is not scrubbed to
|
||||
if (get("preview_dim_previous_layers").empty())
|
||||
set_bool("preview_dim_previous_layers", false);
|
||||
|
||||
// ORCA: brightness of those dimmed layers, in percent. 0 = black, capped at 99 because
|
||||
// 100 would render them unchanged, which is what disabling the option already does
|
||||
if (get("preview_dim_previous_layers_brightness").empty())
|
||||
set("preview_dim_previous_layers_brightness", "40");
|
||||
else {
|
||||
int brightness = 40;
|
||||
try {
|
||||
brightness = std::stoi(get("preview_dim_previous_layers_brightness"));
|
||||
}
|
||||
catch (...) {
|
||||
brightness = 40;
|
||||
}
|
||||
set("preview_dim_previous_layers_brightness", std::to_string(std::max(0, std::min(brightness, 99))));
|
||||
}
|
||||
|
||||
if (get("filaments_area_preferred_count").empty())
|
||||
set("filaments_area_preferred_count", "10");
|
||||
|
||||
@@ -265,6 +280,9 @@ void AppConfig::set_defaults()
|
||||
set(SETTING_OPENGL_FPS_CAP, std::to_string(fps_cap));
|
||||
}
|
||||
|
||||
// The getter already defaults, parses and clamps; write back what it resolves to.
|
||||
set(SETTING_PLUGIN_PAGES_VISIBLE_COUNT, std::to_string(get_plugin_pages_visible_count()));
|
||||
|
||||
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
|
||||
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
|
||||
|
||||
@@ -611,6 +629,12 @@ void AppConfig::set_defaults()
|
||||
set_bool("window_buttons_on_left", false);
|
||||
#endif
|
||||
|
||||
if (get("use_printer_agents").empty())
|
||||
{
|
||||
// false = legacy behavior using print hosts
|
||||
set_bool("use_printer_agents", false);
|
||||
}
|
||||
|
||||
// Remove legacy window positions/sizes
|
||||
erase("app", "main_frame_maximized");
|
||||
erase("app", "main_frame_pos");
|
||||
@@ -862,7 +886,7 @@ std::string AppConfig::load()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(std::exception err) {
|
||||
} catch(const std::exception &err) {
|
||||
BOOST_LOG_TRIVIAL(info) << format("parse app config \"%1%\", error: %2%", AppConfig::loading_path(), err.what());
|
||||
|
||||
return err.what();
|
||||
@@ -1609,6 +1633,22 @@ void AppConfig::set_network_plugin_version(const std::string& version)
|
||||
set(SETTING_NETWORK_PLUGIN_VERSION, version);
|
||||
}
|
||||
|
||||
int AppConfig::get_plugin_pages_visible_count() const
|
||||
{
|
||||
std::string value = get(SETTING_PLUGIN_PAGES_VISIBLE_COUNT);
|
||||
if (value.empty())
|
||||
return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
|
||||
|
||||
int visible_count = PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
|
||||
try {
|
||||
visible_count = std::stoi(value);
|
||||
}
|
||||
catch (...) {
|
||||
return PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT;
|
||||
}
|
||||
return std::clamp(visible_count, PLUGIN_PAGES_VISIBLE_COUNT_MIN, PLUGIN_PAGES_VISIBLE_COUNT_MAX);
|
||||
}
|
||||
|
||||
std::vector<std::string> AppConfig::get_skipped_network_versions() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
|
||||
@@ -41,6 +41,11 @@ using namespace nlohmann;
|
||||
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
|
||||
#define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals"
|
||||
|
||||
#define SETTING_PLUGIN_PAGES_VISIBLE_COUNT "plugin_pages_visible_count"
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_MIN 1
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_DEFAULT 5
|
||||
#define PLUGIN_PAGES_VISIBLE_COUNT_MAX 10
|
||||
|
||||
#if defined(_WIN32) || defined(_WIN64)
|
||||
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.09"
|
||||
#else
|
||||
@@ -374,6 +379,10 @@ public:
|
||||
std::string get_network_plugin_version() const;
|
||||
void set_network_plugin_version(const std::string& version);
|
||||
|
||||
// Number of plugin pages shown as fixed tabs before the rest are collapsed into a
|
||||
// dropdown on the last tab.
|
||||
int get_plugin_pages_visible_count() const;
|
||||
|
||||
std::vector<std::string> get_skipped_network_versions() const;
|
||||
void add_skipped_network_version(const std::string& version);
|
||||
bool is_network_version_skipped(const std::string& version) const;
|
||||
|
||||
@@ -23,14 +23,14 @@ inline coord_t meshfix_maximum_extrusion_area_deviation() { return scaled<coo
|
||||
class WallToolPathsParams
|
||||
{
|
||||
public:
|
||||
float min_bead_width;
|
||||
float min_feature_size;
|
||||
float min_length_factor;
|
||||
float wall_transition_length;
|
||||
float wall_transition_angle;
|
||||
float wall_transition_filter_deviation;
|
||||
int wall_distribution_count;
|
||||
bool is_top_or_bottom_layer;
|
||||
float min_bead_width = 0.f;
|
||||
float min_feature_size = 0.f;
|
||||
float min_length_factor = 0.5f;
|
||||
float wall_transition_length = 0.f;
|
||||
float wall_transition_angle = 10.f;
|
||||
float wall_transition_filter_deviation = 0.f;
|
||||
int wall_distribution_count = 1;
|
||||
bool is_top_or_bottom_layer = false;
|
||||
|
||||
coord_t wall_maximum_resolution = meshfix_maximum_resolution();
|
||||
coord_t wall_maximum_deviation = meshfix_maximum_deviation();
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
namespace Slic3r {
|
||||
|
||||
template BoundingBoxBase<Point, Points>::BoundingBoxBase(const Points &points);
|
||||
template void BoundingBoxBase<Point, Points>::construct<0, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator);
|
||||
template void BoundingBoxBase<Point, Points>::construct<1, BoundingBox, Points::const_iterator>(BoundingBox&, Points::const_iterator, Points::const_iterator);
|
||||
template BoundingBoxBase<Vec2d>::BoundingBoxBase(const std::vector<Vec2d> &points);
|
||||
|
||||
template BoundingBox3Base<Vec3d>::BoundingBox3Base(const std::vector<Vec3d> &points);
|
||||
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
min(p1), max(p1), defined(false) { merge(p2); merge(p3); }
|
||||
|
||||
template<class It, class = IteratorOnly<It>>
|
||||
BoundingBoxBase(It from, It to)
|
||||
BoundingBoxBase(It from, It to) : BoundingBoxBase()
|
||||
{ construct(*this, from, to); }
|
||||
|
||||
BoundingBoxBase(const PointsType &points)
|
||||
|
||||
+20
-18
@@ -32,15 +32,13 @@ static void append_and_translate(ExPolygons &dst, const ExPolygons &src, const P
|
||||
for (; dst_idx < dst.size(); ++dst_idx)
|
||||
dst[dst_idx].translate(instance_shift);
|
||||
}
|
||||
// BBS: generate brim area by objs
|
||||
static void append_and_translate(ExPolygons& dst, const ExPolygons& src,
|
||||
const PrintInstance& instance, size_t instance_idx, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap) {
|
||||
// Orca: Translate the brim area into print coordinates and store it per instance.
|
||||
static void append_and_translate(const ExPolygons& src, const PrintInstance& instance,
|
||||
size_t instance_idx, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap) {
|
||||
ExPolygons srcShifted = src;
|
||||
Point instance_shift = instance.shift_without_plate_offset();
|
||||
for (size_t src_idx = 0; src_idx < srcShifted.size(); ++src_idx)
|
||||
srcShifted[src_idx].translate(instance_shift);
|
||||
srcShifted = diff_ex(srcShifted, dst);
|
||||
//expolygons_append(dst, temp2);
|
||||
for (ExPolygon& expoly : srcShifted)
|
||||
expoly.translate(instance_shift);
|
||||
expolygons_append(brimAreaMap[{ instance.print_object->id(), instance_idx }], std::move(srcShifted));
|
||||
}
|
||||
|
||||
@@ -351,7 +349,7 @@ static ExPolygons make_brim_ears_auto(const ExPolygons& obj_expoly, coord_t size
|
||||
return mouse_ears_ex;
|
||||
}
|
||||
|
||||
static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWidth, float brim_offset, Flow &flow, bool is_outer_brim)
|
||||
static ExPolygons make_brim_ears(const PrintObject* object)
|
||||
{
|
||||
ExPolygons mouse_ears_ex;
|
||||
BrimPoints brim_ear_points = object->model_object()->brim_points;
|
||||
@@ -375,12 +373,7 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
|
||||
Vec3f world_pos = pt.transform(trsf.get_matrix());
|
||||
if ( world_pos.z() > 0) continue;
|
||||
Polygon point_round;
|
||||
float brim_width = floor(scale_(pt.head_front_radius) / flowWidth / 2) * flowWidth * 2;
|
||||
if (is_outer_brim) {
|
||||
double flowWidthScale = flowWidth / SCALING_FACTOR;
|
||||
brim_width = floor(brim_width / flowWidthScale / 2) * flowWidthScale * 2;
|
||||
}
|
||||
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
|
||||
const coord_t size_ear = scale_(pt.head_front_radius);
|
||||
for (size_t i = 0; i < POLY_SIDE_COUNT; i++) {
|
||||
double angle = (2.0 * PI * i) / POLY_SIDE_COUNT;
|
||||
point_round.points.emplace_back(size_ear * cos(angle), size_ear * sin(angle));
|
||||
@@ -454,7 +447,8 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
bool has_brim_auto = object->config().brim_type == btAutoBrim;
|
||||
const bool use_auto_brim_ears = object->config().brim_type == btEar;
|
||||
const bool use_brim_ears = object->config().brim_type == btPainted;
|
||||
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_auto_brim_ears || use_brim_ears;
|
||||
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
|
||||
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
|
||||
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
|
||||
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
|
||||
@@ -533,7 +527,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
auto innerExpoly = offset_ex(ex_poly.contour, brim_offset, jtRound, SCALED_RESOLUTION);
|
||||
ExPolygons outerExpoly;
|
||||
if (use_brim_ears) {
|
||||
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, true);
|
||||
outerExpoly = make_brim_ears(object);
|
||||
//outerExpoly = offset_ex(outerExpoly, brim_width_mod, jtRound, SCALED_RESOLUTION);
|
||||
} else if (use_auto_brim_ears) {
|
||||
coord_t size_ear = (brim_width_mod - brim_offset - flow.scaled_spacing());
|
||||
@@ -547,7 +541,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
ExPolygons outerExpoly;
|
||||
auto innerExpoly = offset_ex(ex_poly_holes_reversed, -brim_width - brim_offset);
|
||||
if (use_brim_ears) {
|
||||
outerExpoly = make_brim_ears(object, flowWidth, brim_offset, flow, false);
|
||||
outerExpoly = make_brim_ears(object);
|
||||
} else if (use_auto_brim_ears) {
|
||||
coord_t size_ear = (brim_width - brim_offset - flow.scaled_spacing());
|
||||
outerExpoly = make_brim_ears_auto(offset_ex(ex_poly_holes_reversed, -brim_offset), size_ear, ear_detection_length, brim_ears_max_angle, false);
|
||||
@@ -572,7 +566,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
for (size_t instance_idx = 0; instance_idx < object->instances().size(); ++instance_idx) {
|
||||
const PrintInstance& instance = object->instances()[instance_idx];
|
||||
if (!brim_area_object.empty())
|
||||
append_and_translate(brim_area, brim_area_object, instance, instance_idx, brimAreaMap);
|
||||
append_and_translate(brim_area_object, instance, instance_idx, brimAreaMap);
|
||||
append_and_translate(no_brim_area, no_brim_area_object, instance);
|
||||
append_and_translate(holes, holes_object, instance);
|
||||
append_and_translate(objectIslands, objectIsland, instance);
|
||||
@@ -875,6 +869,14 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
|
||||
ExPolygons islands_area_ex = outer_inner_brim_area(print,
|
||||
float(flow.scaled_spacing()), brimAreaMap, objPrintVec, printExtruders);
|
||||
|
||||
if (!print.config().combine_brims) {
|
||||
ExPolygons claimed_area;
|
||||
for (auto& [_, areas] : brimAreaMap) {
|
||||
areas = diff_ex(areas, claimed_area);
|
||||
expolygons_append(claimed_area, areas);
|
||||
}
|
||||
}
|
||||
|
||||
// BBS: Find boundingbox of the first layer
|
||||
for (const ObjectID printObjID : print.print_object_ids()) {
|
||||
BoundingBox bbx;
|
||||
|
||||
@@ -19,6 +19,7 @@ if (TARGET OpenVDB::openvdb)
|
||||
endif()
|
||||
|
||||
option(BUILD_SHARED_LIBS "Build shared libs" OFF)
|
||||
option(USE_SLIC3R_CONSOLE_LOG "Enable console logging in RelWithDebInfo builds" OFF)
|
||||
|
||||
set(lisbslic3r_sources
|
||||
AABBMesh.cpp
|
||||
@@ -148,6 +149,8 @@ set(lisbslic3r_sources
|
||||
Fill/FillConcentric.hpp
|
||||
Fill/FillConcentricInternal.cpp
|
||||
Fill/FillConcentricInternal.hpp
|
||||
Fill/FillCornerSmoothing.cpp
|
||||
Fill/FillCornerSmoothing.hpp
|
||||
Fill/Fill.cpp
|
||||
Fill/FillCrossHatch.cpp
|
||||
Fill/FillCrossHatch.hpp
|
||||
@@ -247,6 +250,8 @@ set(lisbslic3r_sources
|
||||
GCode/Thumbnails.hpp
|
||||
GCode/ToolOrdering.cpp
|
||||
GCode/ToolOrdering.hpp
|
||||
GCode/OrderingStrategies.cpp
|
||||
GCode/OrderingStrategies.hpp
|
||||
GCode/WipeTower2.cpp
|
||||
GCode/WipeTower2.hpp
|
||||
GCode/WipeTower.cpp
|
||||
@@ -499,8 +504,12 @@ set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE ON CACHE BOOL "" FORCE)
|
||||
|
||||
cmake_policy(PUSH)
|
||||
cmake_policy(SET CMP0011 NEW)
|
||||
# CGAL's config resets policies (cmake_minimum_required ...3.23), so a plain SET
|
||||
# can't reach it; the default opts its Boost lookup into BoostConfig (CMP0167).
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0167 NEW)
|
||||
find_package(CGAL REQUIRED)
|
||||
find_package(OpenCV REQUIRED core)
|
||||
unset(CMAKE_POLICY_DEFAULT_CMP0167)
|
||||
cmake_policy(POP)
|
||||
|
||||
add_library(libslic3r_cgal STATIC
|
||||
@@ -537,7 +546,9 @@ endif ()
|
||||
encoding_check(libslic3r)
|
||||
|
||||
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
|
||||
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
|
||||
if (USE_SLIC3R_CONSOLE_LOG)
|
||||
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
|
||||
endif()
|
||||
target_include_directories(libslic3r PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_include_directories(libslic3r SYSTEM PUBLIC ${EXPAT_INCLUDE_DIRS})
|
||||
|
||||
@@ -547,7 +558,7 @@ find_package(OpenCASCADE REQUIRED)
|
||||
target_include_directories(libslic3r SYSTEM PUBLIC ${OpenCASCADE_INCLUDE_DIR})
|
||||
|
||||
find_package(JPEG REQUIRED)
|
||||
find_package(draco REQUIRED)
|
||||
find_package(Draco REQUIRED)
|
||||
|
||||
set(OCCT_LIBS
|
||||
TKXDESTEP
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define slic3r_Config_hpp_
|
||||
|
||||
#include <assert.h>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <climits>
|
||||
#include <cfloat>
|
||||
@@ -780,10 +781,14 @@ public:
|
||||
this->values[i] = rhs_vec->values[i];
|
||||
modified = true;
|
||||
} else {
|
||||
if ((i < default_index.size()) && (default_index[i] < default_value.size()))
|
||||
// Orca: a negative slot (failed variant lookup) must not silently collapse the
|
||||
// whole array to the first slot's value — the int-vs-size_t comparison used to
|
||||
// promote -1 past the bounds check. Keep the slot's own value (get_at-style
|
||||
// clamp) when no valid index is available.
|
||||
if ((i < default_index.size()) && (default_index[i] >= 0) && (size_t(default_index[i]) < default_value.size()))
|
||||
this->values[i] = default_value[default_index[i]];
|
||||
else
|
||||
this->values[i] = default_value[0];
|
||||
this->values[i] = default_value[std::min(i, default_value.size() - 1)];
|
||||
}
|
||||
}
|
||||
return modified;
|
||||
@@ -2106,6 +2111,11 @@ public:
|
||||
throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type");
|
||||
// rhs could be of the following type: ConfigOptionEnumGeneric or ConfigOptionEnum<T>
|
||||
this->value = rhs->getInt();
|
||||
// Orca: options embedded in a StaticPrintConfig are constructed without a keys_map;
|
||||
// adopt the source's so a later serialize() can emit names.
|
||||
if (this->keys_map == nullptr)
|
||||
if (auto rhs_generic = dynamic_cast<const ConfigOptionEnumGeneric *>(rhs))
|
||||
this->keys_map = rhs_generic->keys_map;
|
||||
}
|
||||
|
||||
std::string serialize() const override
|
||||
@@ -2162,7 +2172,12 @@ public:
|
||||
if (rhs->type() != this->type())
|
||||
throw ConfigurationError("ConfigOptionEnumGeneric: Assigning an incompatible type");
|
||||
// rhs could be of the following type: ConfigOptionEnumsGeneric
|
||||
this->values = dynamic_cast<const ConfigOptionEnumsGenericTempl *>(rhs)->values;
|
||||
auto rhs_enums = dynamic_cast<const ConfigOptionEnumsGenericTempl *>(rhs);
|
||||
this->values = rhs_enums->values;
|
||||
// Orca: options embedded in a StaticPrintConfig are constructed without a keys_map;
|
||||
// adopt the source's so a later serialize() emits names instead of empty tokens.
|
||||
if (this->keys_map == nullptr)
|
||||
this->keys_map = rhs_enums->keys_map;
|
||||
}
|
||||
|
||||
std::string serialize() const override
|
||||
@@ -2258,6 +2273,8 @@ public:
|
||||
plugin_picker,
|
||||
// Raw JSON string value, edited through a dialog behind a button rather than in the row.
|
||||
plugin_config,
|
||||
// PrinterAgentChoice
|
||||
printer_agent_select,
|
||||
};
|
||||
|
||||
// Identifier of this option. It is stored here so that it is accessible through the by_serialization_key_ordinal map.
|
||||
@@ -2965,6 +2982,8 @@ public:
|
||||
const double & opt_float(const t_config_option_key &opt_key, unsigned int idx) const;
|
||||
double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option<ConfigOptionFloatsNullable>(opt_key)->get_at(idx); }
|
||||
const double & opt_float_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast<const ConfigOptionFloatsNullable *>(this->option(opt_key))->get_at(idx); }
|
||||
FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) { return this->option<ConfigOptionFloatsOrPercentsNullable>(opt_key)->get_at(idx); }
|
||||
const FloatOrPercent & opt_float_or_percent_nullable(const t_config_option_key &opt_key, unsigned int idx) const { return dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(this->option(opt_key))->get_at(idx); }
|
||||
|
||||
int& opt_int(const t_config_option_key &opt_key) { return this->option<ConfigOptionInt>(opt_key)->value; }
|
||||
int opt_int(const t_config_option_key &opt_key) const { return dynamic_cast<const ConfigOptionInt*>(this->option(opt_key))->value; }
|
||||
|
||||
@@ -220,12 +220,12 @@ double Extruder::retract_restart_extra() const
|
||||
|
||||
double Extruder::retract_length_toolchange() const
|
||||
{
|
||||
return m_config->retract_length_toolchange.get_at(extruder_id());
|
||||
return m_config->retract_length_toolchange.get_at(m_config_index);
|
||||
}
|
||||
|
||||
double Extruder::retract_restart_extra_toolchange() const
|
||||
{
|
||||
return m_config->retract_restart_extra_toolchange.get_at(extruder_id());
|
||||
return m_config->retract_restart_extra_toolchange.get_at(m_config_index);
|
||||
}
|
||||
|
||||
double Extruder::travel_slope() const
|
||||
|
||||
@@ -57,6 +57,9 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
||||
if (template_string.empty()) {
|
||||
return Geometry::deg2rad(fixed_infill_angle);
|
||||
}
|
||||
// Convert the id to an index. Layer::id() counts the raft layers, object->layers() does not.
|
||||
const size_t first_object_layer_id = object->get_layer(0)->id();
|
||||
layer_id = layer_id > first_object_layer_id ? layer_id - first_object_layer_id : 0;
|
||||
double angle = 0.0;
|
||||
ConfigOptionFloats rotate_angles;
|
||||
const std::string search_string = "/NnZz$LlUuQq~^|#";
|
||||
@@ -75,6 +78,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
||||
double angle_start = 0;
|
||||
double limit_fill_z = object->get_layer(0)->bottom_z();
|
||||
double start_fill_z = limit_fill_z;
|
||||
// The raft height, or 0 without a raft.
|
||||
const double print_z_offset = object->slicing_parameters().object_print_z_min;
|
||||
bool _noop = false;
|
||||
auto fill_form = std::string::npos;
|
||||
bool _absolute = false;
|
||||
@@ -84,7 +89,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
||||
for (int i = 0; i <= layer_id; i++) {
|
||||
double fill_z = object->get_layer(i)->bottom_z();
|
||||
|
||||
if (limit_fill_z < object->get_layer(i)->slice_z) {
|
||||
// slice_z is measured from the bottom of the model, limit_fill_z from the build plate.
|
||||
if (limit_fill_z < object->get_layer(i)->slice_z + print_z_offset) {
|
||||
if (repeats) { // if repeats >0 then restore parameters for new iteration
|
||||
limit_fill_z += limit_fill_z - start_fill_z;
|
||||
start_fill_z = fill_z;
|
||||
@@ -272,6 +278,9 @@ struct SurfaceFillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" wave.
|
||||
bool gyroid_optimized = false;
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
|
||||
bool separated_infills{false};
|
||||
|
||||
@@ -310,6 +319,7 @@ struct SurfaceFillParams
|
||||
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
||||
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
||||
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
||||
RETURN_COMPARE_NON_EQUAL(smooth_factor);
|
||||
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
||||
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
||||
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, fill_order);
|
||||
@@ -342,6 +352,7 @@ struct SurfaceFillParams
|
||||
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
||||
this->separated_infills == rhs.separated_infills &&
|
||||
this->gyroid_optimized == rhs.gyroid_optimized &&
|
||||
this->smooth_factor == rhs.smooth_factor &&
|
||||
this->fill_order == rhs.fill_order;
|
||||
}
|
||||
};
|
||||
@@ -958,6 +969,11 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
||||
region_config.sparse_infill_rotate_template.value);
|
||||
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
|
||||
|
||||
// Orca: the smoothing factor only applies to the sparse infill patterns that
|
||||
// implement it. The fills clamp and validate the value themselves.
|
||||
if (is_smoothable_infill_pattern(params.pattern, params.multiline))
|
||||
params.smooth_factor = 0.01 * region_config.sparse_infill_smooth_factor.value;
|
||||
} else {
|
||||
const bool top_layer_direction_set = surface.is_top() && region_config.top_layer_direction.value >= 0.;
|
||||
const bool bottom_layer_direction_set = surface.is_bottom() && region_config.bottom_layer_direction.value >= 0.;
|
||||
@@ -1322,6 +1338,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
// BBS
|
||||
params.flow = surface_fill.params.flow;
|
||||
@@ -1563,6 +1580,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
for (ExPolygon &expoly : surface_fill.expolygons) {
|
||||
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../ShortestPath.hpp"
|
||||
#include "../Surface.hpp"
|
||||
#include "FillBase.hpp"
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
#include "Fill3DHoneycomb.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -271,6 +272,9 @@ void Fill3DHoneycomb::_fill_surface_single(
|
||||
for (Polyline &pl : polylines){
|
||||
pl.translate(bb.min);
|
||||
pl.simplify(5 * spacing); // simplify to 5x line width
|
||||
// Orca: round the corners of the octahedral wave. The layers where the wave degenerates to a
|
||||
// straight line have no corner to round.
|
||||
smooth_polyline_corners(pl, params.smooth_factor, scaled<double>(params.resolution));
|
||||
}
|
||||
|
||||
// Apply multiline offset if needed
|
||||
|
||||
@@ -1857,12 +1857,12 @@ static inline void base_support_extend_infill_lines(Polylines &infill, BoundaryI
|
||||
const bool first = graph.first(cp);
|
||||
int extend_next_idx = -1;
|
||||
int extend_prev_idx = -1;
|
||||
coord_t dist_y_prev;
|
||||
coord_t dist_y_next;
|
||||
double arc_len_prev;
|
||||
double arc_len_next;
|
||||
coord_t dist_y_prev = 0;
|
||||
coord_t dist_y_next = 0;
|
||||
double arc_len_prev = 0;
|
||||
double arc_len_next = 0;
|
||||
|
||||
if (! graph.next_vertical(cp)){
|
||||
if (! graph.next_vertical(cp)) {
|
||||
size_t i = cp.point_idx;
|
||||
size_t j = next_idx_modulo(i, contour);
|
||||
while (j != cp.next_on_contour->point_idx) {
|
||||
|
||||
@@ -82,6 +82,9 @@ struct FillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" variant.
|
||||
bool gyroid_optimized { false };
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
// For Lateral lattice
|
||||
coordf_t lateral_lattice_angle_1 { 0.f };
|
||||
coordf_t lateral_lattice_angle_2 { 0.f };
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "Arachne/WallToolPaths.hpp"
|
||||
|
||||
#include "FillConcentric.hpp"
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
#include <libslic3r/ShortestPath.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -32,12 +33,32 @@ void FillConcentric::_fill_surface_single(
|
||||
|
||||
Polygons loops = to_polygons(contracted);
|
||||
|
||||
ExPolygons last { std::move(contracted) };
|
||||
ExPolygons last { contracted };
|
||||
while (! last.empty()) {
|
||||
last = offset2_ex(last, -(distance + min_spacing/2), +min_spacing/2);
|
||||
append(loops, to_polygons(last));
|
||||
}
|
||||
|
||||
// Orca: round the corners of the loops. Unlike the other patterns these are never clipped to the
|
||||
// fill region - they are its offsets - so a corner may only be rounded where the curve replacing it
|
||||
// stays inside. Rounding cuts toward the inside of the turn, which around a hole, at a concave
|
||||
// feature or across a thin region is outside the fill and would put the extrusion over a wall.
|
||||
// The reach is capped at half the distance between two loops as well: a loop is as long as the
|
||||
// object, and a corner cut by half of its side would swallow the neighbouring loops.
|
||||
auto corner_stays_inside = [&contracted](const Vec2d &from, const Vec2d &to) {
|
||||
// The straight chord between the ends of the curve is the deepest the curve can cut.
|
||||
for (const double t : { 0.25, 0.5, 0.75 }) {
|
||||
const Vec2d sample = from + t * (to - from);
|
||||
const Point point(coord_t(sample.x()), coord_t(sample.y()));
|
||||
if (std::none_of(contracted.begin(), contracted.end(),
|
||||
[&point](const ExPolygon ®ion) { return region.contains(point); }))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
smooth_polygons_corners(loops, params.smooth_factor, scaled<double>(params.resolution), 0.5 * distance,
|
||||
corner_stays_inside);
|
||||
|
||||
// generate paths from the outermost to the innermost, to avoid
|
||||
// adhesion problems of the first central tiny loops
|
||||
loops = union_pt_chained_outside_in(loops);
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#include <array>
|
||||
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Turns sharper than this are left untouched: both ends of the curve replacing such a corner nearly
|
||||
// coincide, so the corner would be rounded into a degenerate loop instead of a hairpin.
|
||||
static constexpr const double min_smoothed_turn_cosine = -0.9;
|
||||
|
||||
// The control points are expressed in the (incoming, outgoing) basis of the corner, which is not
|
||||
// orthonormal for turns other than a right angle.
|
||||
using QuinticBezier = std::array<Vec2d, 6>;
|
||||
|
||||
static bool is_bezier_flat(const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation)
|
||||
{
|
||||
// A Bezier curve stays inside the convex hull of its control points. Therefore, keeping every
|
||||
// control point within a deviation-wide strip around the endpoint chord conservatively bounds the
|
||||
// flattening error. The cross product is the perpendicular distance scaled by the chord length;
|
||||
// comparing squared values avoids a square root.
|
||||
auto in_plane = [&incoming, &outgoing](const Vec2d &c) { return c.x() * incoming + c.y() * outgoing; };
|
||||
const Vec2d chord = in_plane(curve.back() - curve.front());
|
||||
const double chord_length_sq = chord.squaredNorm();
|
||||
const double max_cross_sq = deviation * deviation * chord_length_sq;
|
||||
|
||||
for (size_t i = 1; i + 1 < curve.size(); ++i) {
|
||||
const Vec2d offset = in_plane(curve[i] - curve.front());
|
||||
const double cross = chord.x() * offset.y() - chord.y() * offset.x();
|
||||
if (cross * cross > max_cross_sq)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void subdivide_bezier(const QuinticBezier &curve, QuinticBezier &left, QuinticBezier &right)
|
||||
{
|
||||
// Split the curve at t = 0.5 using de Casteljau's algorithm. Each averaging level contributes one
|
||||
// control point to the left half and one to the right half; the latter is filled backwards to keep
|
||||
// both resulting control polygons in their original parameter direction.
|
||||
QuinticBezier subdivision = curve;
|
||||
left.front() = subdivision.front();
|
||||
right.back() = subdivision.back();
|
||||
for (size_t level = 1; level < curve.size(); ++level) {
|
||||
for (size_t i = 0; i + level < curve.size(); ++i)
|
||||
subdivision[i] = 0.5 * (subdivision[i] + subdivision[i + 1]);
|
||||
left[level] = subdivision.front();
|
||||
right[curve.size() - level - 1] = subdivision[curve.size() - level - 1];
|
||||
}
|
||||
}
|
||||
|
||||
static void flatten_bezier(
|
||||
const QuinticBezier &curve, const Vec2d &incoming, const Vec2d &outgoing, const double deviation, std::vector<Vec2d> &output)
|
||||
{
|
||||
// Subdivide to at least depth 1 so a rounded corner cannot collapse to a single diagonal chord.
|
||||
// A uniform subdivision depth keeps samples at equal parameter intervals t = k / 2^depth,
|
||||
// avoiding abrupt segment-length jumps at adaptive-depth boundaries.
|
||||
static constexpr size_t max_depth = 16;
|
||||
|
||||
std::vector<QuinticBezier> subcurves(2);
|
||||
subdivide_bezier(curve, subcurves[0], subcurves[1]);
|
||||
|
||||
for (size_t depth = 1; depth < max_depth; ++depth) {
|
||||
bool all_flat = true;
|
||||
for (const QuinticBezier &c : subcurves)
|
||||
if (!is_bezier_flat(c, incoming, outgoing, deviation)) {
|
||||
all_flat = false;
|
||||
break;
|
||||
}
|
||||
if (all_flat)
|
||||
break;
|
||||
std::vector<QuinticBezier> finer(subcurves.size() * 2);
|
||||
for (size_t i = 0; i < subcurves.size(); ++i)
|
||||
subdivide_bezier(subcurves[i], finer[i * 2], finer[i * 2 + 1]);
|
||||
subcurves = std::move(finer);
|
||||
}
|
||||
|
||||
// The curve start is deliberately omitted so it can be shared with the straight leg feeding into it.
|
||||
output.clear();
|
||||
output.reserve(subcurves.size());
|
||||
for (const QuinticBezier &c : subcurves)
|
||||
output.emplace_back(c.back());
|
||||
}
|
||||
|
||||
const std::vector<Vec2d>& CornerSmoother::curve_coefficients(
|
||||
const double corner_distance, const Vec2d &incoming, const Vec2d &outgoing)
|
||||
{
|
||||
const double cosine = incoming.dot(outgoing);
|
||||
// Corners of the same size and turn angle are congruent, so they flatten identically. An infill
|
||||
// path walks over the very same corner over and over again, the Hilbert curve over a single one.
|
||||
if (m_has_cached_coefficients && corner_distance == m_cached_distance && cosine == m_cached_cosine)
|
||||
return m_cached_coefficients;
|
||||
|
||||
// One canonical corner running from -corner_distance along the incoming leg to corner_distance
|
||||
// along the outgoing one. At each end, the first three control points are collinear and equally
|
||||
// spaced: the tangent follows the adjoining straight leg and the second derivative is zero. The
|
||||
// endpoint curvature is therefore zero, giving G2 joins to both legs.
|
||||
const double d = corner_distance;
|
||||
const QuinticBezier corner_curve {{
|
||||
{-d, 0.}, {-0.7 * d, 0.}, {-0.4 * d, 0.}, {0., 0.4 * d}, {0., 0.7 * d}, {0., d}
|
||||
}};
|
||||
// Retain a finite positive tolerance if the smoother was set up with an invalid one.
|
||||
const double deviation = m_tolerance > 0. && std::isfinite(m_tolerance) ? m_tolerance : EPSILON;
|
||||
flatten_bezier(corner_curve, incoming, outgoing, deviation, m_cached_coefficients);
|
||||
|
||||
m_cached_distance = corner_distance;
|
||||
m_cached_cosine = cosine;
|
||||
m_has_cached_coefficients = true;
|
||||
return m_cached_coefficients;
|
||||
}
|
||||
|
||||
void CornerSmoother::round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next)
|
||||
{
|
||||
m_corner_points.clear();
|
||||
|
||||
const Vec2d incoming_leg = corner - previous;
|
||||
const Vec2d outgoing_leg = next - corner;
|
||||
const double incoming_length = incoming_leg.norm();
|
||||
const double outgoing_length = outgoing_leg.norm();
|
||||
if (incoming_length < EPSILON || outgoing_length < EPSILON) {
|
||||
m_corner_points.emplace_back(corner);
|
||||
return;
|
||||
}
|
||||
|
||||
const Vec2d incoming = incoming_leg / incoming_length;
|
||||
const Vec2d outgoing = outgoing_leg / outgoing_length;
|
||||
const double cross = incoming.x() * outgoing.y() - incoming.y() * outgoing.x();
|
||||
// A collinear vertex is no corner at all, and a hairpin cannot be rounded, see above.
|
||||
if (std::abs(cross) < EPSILON || incoming.dot(outgoing) < min_smoothed_turn_cosine) {
|
||||
m_corner_points.emplace_back(corner);
|
||||
return;
|
||||
}
|
||||
|
||||
// Consuming at most half of the shorter leg keeps the curves of two adjacent corners apart.
|
||||
double corner_distance = m_corner_distance_ratio * std::min(incoming_length, outgoing_length);
|
||||
if (m_max_corner_distance > 0.)
|
||||
corner_distance = std::min(corner_distance, m_max_corner_distance);
|
||||
|
||||
const Vec2d curve_start = corner - corner_distance * incoming;
|
||||
const Vec2d curve_end = corner + corner_distance * outgoing;
|
||||
if (m_corner_filter && !m_corner_filter(curve_start, curve_end)) {
|
||||
m_corner_points.emplace_back(corner);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<Vec2d> &coefficients = curve_coefficients(corner_distance, incoming, outgoing);
|
||||
m_corner_points.reserve(coefficients.size() + 1);
|
||||
m_corner_points.emplace_back(curve_start);
|
||||
for (const Vec2d &coefficient : coefficients)
|
||||
m_corner_points.emplace_back(corner + coefficient.x() * incoming + coefficient.y() * outgoing);
|
||||
}
|
||||
|
||||
// Rounds the corners of a scaled point sequence. A polygon closes implicitly, so all of its vertices
|
||||
// are corners; a polyline is an open path that keeps both of its ends, even where they coincide - a
|
||||
// path returning to where it started retraces its way back and is not a loop.
|
||||
static Points smooth_corners(const Points &points, const bool polygon, CornerSmoother &smoother)
|
||||
{
|
||||
// A polygon has no free ends, so its first vertex is a corner like any other. Rounding it takes
|
||||
// feeding the smoother the last vertex first, whose own output point is then dropped again.
|
||||
size_t skip = polygon ? 1 : 0;
|
||||
|
||||
Points smoothed;
|
||||
smoothed.reserve(2 * points.size());
|
||||
auto emit = [&smoothed, &skip](const Vec2d &point) {
|
||||
if (skip > 0) {
|
||||
--skip;
|
||||
return;
|
||||
}
|
||||
smoothed.emplace_back(coord_t(std::floor(point.x() + 0.5)), coord_t(std::floor(point.y() + 0.5)));
|
||||
};
|
||||
|
||||
if (polygon)
|
||||
smoother.push(points.back().cast<double>(), emit);
|
||||
for (const Point &point : points)
|
||||
smoother.push(point.cast<double>(), emit);
|
||||
if (polygon)
|
||||
// Wrap the first vertex around, so that the last one is a corner as well.
|
||||
smoother.push(points.front().cast<double>(), emit);
|
||||
smoother.flush(emit);
|
||||
|
||||
if (polygon)
|
||||
// The flushed point is the wrapped first vertex, which a polygon does not store.
|
||||
smoothed.pop_back();
|
||||
return smoothed;
|
||||
}
|
||||
|
||||
void smooth_polyline_corners(Polyline &polyline, const double smooth_factor, const double tolerance,
|
||||
const double max_corner_distance, const CornerFilter &corner_filter)
|
||||
{
|
||||
CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter);
|
||||
if (!smoother.enabled() || polyline.size() < 3)
|
||||
return;
|
||||
|
||||
polyline.points = smooth_corners(polyline.points, false, smoother);
|
||||
// Rounding back to the integer grid may collapse neighbouring samples of a curve.
|
||||
polyline.remove_duplicate_points();
|
||||
}
|
||||
|
||||
void smooth_polylines_corners(Polylines &polylines, const double smooth_factor, const double tolerance,
|
||||
const double max_corner_distance, const CornerFilter &corner_filter)
|
||||
{
|
||||
if (sanitize_smooth_factor(smooth_factor) == 0.)
|
||||
return;
|
||||
for (Polyline &polyline : polylines)
|
||||
smooth_polyline_corners(polyline, smooth_factor, tolerance, max_corner_distance, corner_filter);
|
||||
}
|
||||
|
||||
void smooth_polygons_corners(Polygons &polygons, const double smooth_factor, const double tolerance,
|
||||
const double max_corner_distance, const CornerFilter &corner_filter)
|
||||
{
|
||||
CornerSmoother smoother(smooth_factor, tolerance, max_corner_distance, corner_filter);
|
||||
if (!smoother.enabled())
|
||||
return;
|
||||
|
||||
for (Polygon &polygon : polygons) {
|
||||
if (polygon.size() < 3)
|
||||
continue;
|
||||
polygon.points = smooth_corners(polygon.points, true, smoother);
|
||||
polygon.remove_duplicate_points();
|
||||
// The curves of the first and of the last corner may have met on the segment they share. A
|
||||
// polygon closes implicitly, so it must not repeat its first vertex at the end.
|
||||
if (polygon.points.size() > 1 && polygon.points.front() == polygon.points.back())
|
||||
polygon.points.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
#include "../Polygon.hpp"
|
||||
#include "../Polyline.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Orca: NaN or infinite factors disable the smoothing, everything else is clamped to <0, 1>.
|
||||
inline double sanitize_smooth_factor(double smooth_factor)
|
||||
{
|
||||
return std::isfinite(smooth_factor) ? std::clamp(smooth_factor, 0., 1.) : 0.;
|
||||
}
|
||||
|
||||
// Decides whether a corner may be replaced by the curve that leaves the path at `from` and rejoins it
|
||||
// at `to`, both in the coordinate system of the pushed points. Rounding cuts toward the inside of the
|
||||
// turn, so a path that is not clipped to the fill region afterwards needs this to stay inside it.
|
||||
using CornerFilter = std::function<bool(const Vec2d &from, const Vec2d &to)>;
|
||||
|
||||
// Orca: Replaces the sharp vertices of an infill path with curves that join the adjoining straight
|
||||
// legs with a continuous curvature, so the toolhead does not have to stop in every corner.
|
||||
// Points are pushed one by one, because the plane path fills produce their path on the fly, and
|
||||
// every point of the smoothed path is handed over to the caller supplied emit callback.
|
||||
// Fully smoothed adjacent corners meet at the midpoint of the segment they share, so the emitted
|
||||
// points may collapse onto each other once rounded to the integer grid of the caller. Dropping such
|
||||
// duplicates is left to the caller, which is the only one knowing that grid.
|
||||
class CornerSmoother
|
||||
{
|
||||
public:
|
||||
// tolerance is the maximum chordal deviation of the flattened curves, in the units of the pushed
|
||||
// points. max_corner_distance caps how far a curve may reach along a leg, in the same units; it
|
||||
// bounds how far a rounded corner moves away from the original path, which matters where the legs
|
||||
// are much longer than the spacing of the pattern. Zero leaves the reach uncapped.
|
||||
CornerSmoother(double smooth_factor, double tolerance, double max_corner_distance = 0.,
|
||||
CornerFilter corner_filter = {})
|
||||
: m_corner_distance_ratio(0.5 * sanitize_smooth_factor(smooth_factor)), m_tolerance(tolerance),
|
||||
m_max_corner_distance(max_corner_distance), m_corner_filter(std::move(corner_filter))
|
||||
{}
|
||||
|
||||
bool enabled() const { return m_corner_distance_ratio > 0.; }
|
||||
|
||||
template<typename Emit> void push(const Vec2d &point, Emit &emit)
|
||||
{
|
||||
if (m_pending == 0) {
|
||||
emit(point);
|
||||
m_previous = point;
|
||||
} else if (m_pending > 1) {
|
||||
round_corner(m_previous, m_corner, point);
|
||||
for (const Vec2d &corner_point : m_corner_points)
|
||||
emit(corner_point);
|
||||
m_previous = m_corner;
|
||||
}
|
||||
m_corner = point;
|
||||
m_pending = std::min(m_pending + 1, 2);
|
||||
}
|
||||
|
||||
// Emits the last point of the path and prepares the smoother for a new one.
|
||||
template<typename Emit> void flush(Emit &emit)
|
||||
{
|
||||
if (m_pending > 1)
|
||||
emit(m_corner);
|
||||
m_pending = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
// Fills m_corner_points with the points replacing the corner vertex.
|
||||
void round_corner(const Vec2d &previous, const Vec2d &corner, const Vec2d &next);
|
||||
// Flattens the canonical corner curve of the given size and turn into coordinates of the
|
||||
// (incoming, outgoing) basis of the corner. Cached, as an infill path repeats the same corner.
|
||||
const std::vector<Vec2d>& curve_coefficients(double corner_distance, const Vec2d &incoming, const Vec2d &outgoing);
|
||||
|
||||
// Fraction of the shorter adjoining segment consumed on each side of a corner. Half of a segment
|
||||
// is the maximum, otherwise the curves of two adjacent corners would overlap.
|
||||
const double m_corner_distance_ratio;
|
||||
const double m_tolerance;
|
||||
const double m_max_corner_distance;
|
||||
const CornerFilter m_corner_filter;
|
||||
std::vector<Vec2d> m_corner_points;
|
||||
// Cached flattening of the last corner, valid for corners of the same size and turn angle.
|
||||
std::vector<Vec2d> m_cached_coefficients;
|
||||
double m_cached_distance { 0. };
|
||||
double m_cached_cosine { 0. };
|
||||
bool m_has_cached_coefficients { false };
|
||||
|
||||
Vec2d m_previous { Vec2d::Zero() };
|
||||
Vec2d m_corner { Vec2d::Zero() };
|
||||
// Number of points held back: none, the first point of a path, or a corner candidate.
|
||||
int m_pending { 0 };
|
||||
};
|
||||
|
||||
// Rounds the corners of already scaled paths in place. Paths of less than three points are left alone.
|
||||
// Both ends of a polyline are kept where they are, even when they coincide: such a path retraces its
|
||||
// way back and joining its ends would turn it into a loop. See CornerSmoother for max_corner_distance.
|
||||
void smooth_polyline_corners(Polyline &polyline, double smooth_factor, double tolerance,
|
||||
double max_corner_distance = 0., const CornerFilter &corner_filter = {});
|
||||
void smooth_polylines_corners(Polylines &polylines, double smooth_factor, double tolerance,
|
||||
double max_corner_distance = 0., const CornerFilter &corner_filter = {});
|
||||
// Polygons close implicitly, so every one of their vertices is a corner.
|
||||
void smooth_polygons_corners(Polygons &polygons, double smooth_factor, double tolerance,
|
||||
double max_corner_distance = 0., const CornerFilter &corner_filter = {});
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "../Surface.hpp"
|
||||
#include <cmath>
|
||||
#include "FillBase.hpp"
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
#include "FillCrossHatch.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -205,6 +206,9 @@ void FillCrossHatch ::_fill_surface_single(
|
||||
// shift the pattern to the actual space
|
||||
for (Polyline &pl : polylines) { pl.translate(bb.min); }
|
||||
|
||||
// Orca: round the corners of the transition layers. The repeat layers are straight lines and stay as they are.
|
||||
smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution));
|
||||
|
||||
// Apply multiline offset if needed
|
||||
multiline_fill(polylines, params, spacing);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../ShortestPath.hpp"
|
||||
#include "../Surface.hpp"
|
||||
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
#include "FillHoneycomb.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -70,6 +71,9 @@ void FillHoneycomb::_fill_surface_single(
|
||||
}
|
||||
p.rotate(-direction.first, m.hex_center);
|
||||
p.simplify(5 * spacing); // simplify to 5x line width
|
||||
// Orca: round the corners of the honeycomb cells. Done before the clipping, so that the
|
||||
// curves are cut by the region boundary just like the sharp path would be.
|
||||
smooth_polyline_corners(p, params.smooth_factor, scaled<double>(params.resolution));
|
||||
all_polylines.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../Print.hpp"
|
||||
#include "../ShortestPath.hpp"
|
||||
#include "FillBase.hpp"
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
#include "FillLightning.hpp"
|
||||
#include "Lightning/Generator.hpp"
|
||||
|
||||
@@ -17,6 +18,19 @@ void Filler::_fill_surface_single(
|
||||
const Layer &layer = generator->getTreesForLayer(this->layer_id);
|
||||
Polylines fill_lines = layer.convertToLines(to_polygons(expolygon), scaled<coord_t>(0.5 * this->spacing - this->overlap));
|
||||
|
||||
// Orca: round the turns of the branches. Hairpins are left sharp, as they cannot be rounded, and
|
||||
// the reach is capped: cutting a corner moves the branch, and a branch is as long as the object
|
||||
// rather than as long as one cell of a pattern, so half of a leg would merge it with its neighbour
|
||||
// instead of rounding the turn between them. Half the distance between two branches keeps them
|
||||
// apart. With more than one line per infill wall the branches are printed as outlines drawn around
|
||||
// them, and the outlines of branches that run into each other merge into a single one; moving a
|
||||
// branch by more than a fraction of its printed width breaks such an outline up into separate
|
||||
// loops, so that width bounds the reach as well.
|
||||
const double branch_width = scaled<double>(this->spacing) * params.multiline;
|
||||
const double branch_spacing = branch_width / std::max(double(params.density), EPSILON);
|
||||
const double max_reach = 0.5 * (params.multiline > 1 ? branch_width : branch_spacing);
|
||||
smooth_polylines_corners(fill_lines, params.smooth_factor, scaled<double>(params.resolution), max_reach);
|
||||
|
||||
// Apply multiline offset if needed
|
||||
multiline_fill(fill_lines, params, spacing);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "../ShortestPath.hpp"
|
||||
#include "../Surface.hpp"
|
||||
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
#include "FillPlanePath.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -114,12 +115,12 @@ void FillPlanePath::_fill_surface_single(
|
||||
// Filling in a bounding box over the whole object, clip generated polyline against the snug bounding box.
|
||||
snug_bounding_box.translate(-shift.x(), -shift.y());
|
||||
InfillPolylineClipper output(snug_bounding_box, distance_between_lines);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
|
||||
polyline.points = std::move(output.result());
|
||||
} else {
|
||||
// Filling in a snug bounding box, no need to clip.
|
||||
InfillPolylineOutput output(distance_between_lines);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, params, output);
|
||||
polyline.points = std::move(output.result());
|
||||
}
|
||||
}
|
||||
@@ -288,6 +289,62 @@ static void generate_hilbert_curve(coord_t min_x, coord_t min_y, coord_t max_x,
|
||||
}
|
||||
}
|
||||
|
||||
// Rounds the corners of the generated path on its way to the infill output.
|
||||
template<typename Output>
|
||||
class SmoothingPolylineOutput
|
||||
{
|
||||
public:
|
||||
SmoothingPolylineOutput(Output &output, const double smooth_factor, const double tolerance)
|
||||
: m_output(output), m_smoother(smooth_factor, tolerance) {}
|
||||
|
||||
void reserve(size_t n) { m_output.reserve(n); }
|
||||
void add_point(const Vec2d &pt) { auto emit = emitter(); m_smoother.push(pt, emit); }
|
||||
// The smoother holds back the last point of the path until it knows there is no corner left to round.
|
||||
void finish() { auto emit = emitter(); m_smoother.flush(emit); }
|
||||
|
||||
private:
|
||||
// The curves of two adjacent corners meet at the midpoint of the segment they share, where they
|
||||
// may round to the very same output point. Drop those, they would be zero length extrusions.
|
||||
auto emitter()
|
||||
{
|
||||
return [this](const Vec2d &pt) {
|
||||
const Point snapped = m_output.scaled(pt);
|
||||
if (m_has_last_snapped && snapped == m_last_snapped)
|
||||
return;
|
||||
m_last_snapped = snapped;
|
||||
m_has_last_snapped = true;
|
||||
m_output.add_point(pt);
|
||||
};
|
||||
}
|
||||
|
||||
Output &m_output;
|
||||
CornerSmoother m_smoother;
|
||||
Point m_last_snapped { Point::Zero() };
|
||||
bool m_has_last_snapped { false };
|
||||
};
|
||||
|
||||
// Runs the path generator against the concrete output type, optionally through the corner smoother.
|
||||
// The outputs do not share a virtual add_point(), so the type has to be resolved here.
|
||||
template<typename GenerateFn>
|
||||
static void generate_path(InfillPolylineOutput &output, const FillParams ¶ms, const double resolution, GenerateFn generate)
|
||||
{
|
||||
const double smooth_factor = sanitize_smooth_factor(params.smooth_factor);
|
||||
auto run = [smooth_factor, resolution, &generate](auto &out) {
|
||||
if (smooth_factor == 0.) {
|
||||
generate(out);
|
||||
} else {
|
||||
SmoothingPolylineOutput<std::remove_reference_t<decltype(out)>> smoothing(out, smooth_factor, resolution);
|
||||
generate(smoothing);
|
||||
smoothing.finish();
|
||||
}
|
||||
};
|
||||
|
||||
if (output.clips())
|
||||
run(static_cast<InfillPolylineClipper&>(output));
|
||||
else
|
||||
run(output);
|
||||
}
|
||||
|
||||
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double /* resolution */, InfillPolylineOutput &output)
|
||||
{
|
||||
if (output.clips())
|
||||
@@ -296,6 +353,13 @@ void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coo
|
||||
generate_hilbert_curve(min_x, min_y, max_x, max_y, output);
|
||||
}
|
||||
|
||||
void FillHilbertCurve::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams ¶ms, InfillPolylineOutput &output)
|
||||
{
|
||||
generate_path(output, params, resolution,
|
||||
[min_x, min_y, max_x, max_y](auto &out) { generate_hilbert_curve(min_x, min_y, max_x, max_y, out); });
|
||||
}
|
||||
|
||||
template<typename Output>
|
||||
static void generate_octagram_spiral(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, Output &output)
|
||||
{
|
||||
@@ -336,4 +400,11 @@ void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, c
|
||||
generate_octagram_spiral(min_x, min_y, max_x, max_y, output);
|
||||
}
|
||||
|
||||
void FillOctagramSpiral::generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams ¶ms, InfillPolylineOutput &output)
|
||||
{
|
||||
generate_path(output, params, resolution,
|
||||
[min_x, min_y, max_x, max_y](auto &out) { generate_octagram_spiral(min_x, min_y, max_x, max_y, out); });
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -21,10 +21,10 @@ public:
|
||||
void add_point(const Vec2d& pt) { m_out.emplace_back(this->scaled(pt)); }
|
||||
Points&& result() { return std::move(m_out); }
|
||||
virtual bool clips() const { return false; }
|
||||
|
||||
protected:
|
||||
// The output grid the generated points are snapped to.
|
||||
const Point scaled(const Vec2d& fpt) const { return { coord_t(floor(fpt.x() * m_scale_out + 0.5)), coord_t(floor(fpt.y() * m_scale_out + 0.5)) }; }
|
||||
|
||||
protected:
|
||||
// Output polyline.
|
||||
Points m_out;
|
||||
|
||||
@@ -53,6 +53,11 @@ protected:
|
||||
friend class InfillPolylineClipper;
|
||||
|
||||
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) = 0;
|
||||
virtual void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams & /* params */, InfillPolylineOutput &output)
|
||||
{
|
||||
this->generate(min_x, min_y, max_x, max_y, resolution, output);
|
||||
}
|
||||
};
|
||||
|
||||
class FillArchimedeanChords : public FillPlanePath
|
||||
@@ -75,6 +80,8 @@ public:
|
||||
protected:
|
||||
bool centered() const override { return false; }
|
||||
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
|
||||
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams ¶ms, InfillPolylineOutput &output) override;
|
||||
};
|
||||
|
||||
class FillOctagramSpiral : public FillPlanePath
|
||||
@@ -86,6 +93,8 @@ public:
|
||||
protected:
|
||||
bool centered() const override { return true; }
|
||||
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution, InfillPolylineOutput &output) override;
|
||||
void generate(coord_t min_x, coord_t min_y, coord_t max_x, coord_t max_y, const double resolution,
|
||||
const FillParams ¶ms, InfillPolylineOutput &output) override;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "../ShortestPath.hpp"
|
||||
#include "../VariableWidth.hpp"
|
||||
|
||||
#include "FillCornerSmoothing.hpp"
|
||||
#include "FillRectilinear.hpp"
|
||||
|
||||
// #define SLIC3R_DEBUG
|
||||
@@ -3364,6 +3365,10 @@ bool FillRectilinear::fill_surface_trapezoidal(
|
||||
for (Polyline &pl : polylines)
|
||||
pl.translate(rotate_vector.second);
|
||||
|
||||
// Orca: round the corners of the trapezoids. The straight base lines of the triangular family
|
||||
// have no corner to round.
|
||||
smooth_polylines_corners(polylines, params.smooth_factor, scaled<double>(params.resolution));
|
||||
|
||||
// Apply multiline fill
|
||||
multiline_fill(polylines, params, spacing);
|
||||
|
||||
@@ -3576,7 +3581,7 @@ Polylines FillLateralHoneycomb::fill_surface(const Surface *surface, const FillP
|
||||
// |
|
||||
// |
|
||||
// 0 --+--
|
||||
// / \
|
||||
// ⟋ ⟍
|
||||
// why inverted?
|
||||
// it makes determining some of the properties easier
|
||||
// and the two angled legs provide additional horizontal stiffness
|
||||
|
||||
@@ -712,7 +712,7 @@ unsigned int Step::get_triangle_num(double linear_deflection, double angle_defle
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
} catch(const Exception &e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ public:
|
||||
Standard_Boolean UserBreak() override { return should_stop.load(); }
|
||||
|
||||
void Show(const Message_ProgressScope&, const Standard_Boolean) override {
|
||||
std::cout << "Progress: " << GetPosition() << "%" << std::endl;
|
||||
std::cout << "Progress: " << std::fixed << std::setprecision(2) << 100.0 * GetPosition() << "%" << std::endl;
|
||||
}
|
||||
private:
|
||||
std::atomic<bool>& should_stop;
|
||||
|
||||
+451
-130
@@ -13,7 +13,9 @@
|
||||
#include "GCode/PrintExtents.hpp"
|
||||
#include "GCode/Thumbnails.hpp"
|
||||
#include "GCode/WipeTower.hpp"
|
||||
#include "GCode/WipeTower2.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "GCode/OrderingStrategies.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
@@ -730,30 +732,67 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return temp_set_by_gcode;
|
||||
}
|
||||
|
||||
struct CustomGCodeMotionStateChanges
|
||||
{
|
||||
bool acceleration = false;
|
||||
bool jerk = false;
|
||||
};
|
||||
|
||||
static bool custom_gcode_line_has_xy_parameter(const std::string &raw)
|
||||
{
|
||||
const size_t comment_pos = raw.find(';');
|
||||
const std::string_view code(raw.data(), comment_pos == std::string::npos ? raw.size() : comment_pos);
|
||||
return code.find_first_of("XxYy") != std::string_view::npos;
|
||||
}
|
||||
|
||||
static CustomGCodeMotionStateChanges custom_gcode_motion_state_changes(const std::string &gcode)
|
||||
{
|
||||
CustomGCodeMotionStateChanges changes;
|
||||
GCodeReader parser;
|
||||
parser.parse_buffer(gcode, [&changes](GCodeReader &parser, const GCodeReader::GCodeLine &line) {
|
||||
const std::string_view cmd = line.cmd();
|
||||
if (boost::iequals(cmd, "M204") || boost::iequals(cmd, "M201") ||
|
||||
boost::iequals(cmd, "M202"))
|
||||
changes.acceleration = true;
|
||||
else if ((boost::iequals(cmd, "M205") || boost::iequals(cmd, "M207") || boost::iequals(cmd, "M566")) &&
|
||||
custom_gcode_line_has_xy_parameter(line.raw()))
|
||||
changes.jerk = true;
|
||||
else if (boost::iequals(cmd, "SET_VELOCITY_LIMIT")) {
|
||||
changes.acceleration |= boost::icontains(line.raw(), "ACCEL=");
|
||||
changes.jerk |= boost::icontains(line.raw(), "SQUARE_CORNER_VELOCITY=");
|
||||
}
|
||||
|
||||
if (changes.acceleration && changes.jerk)
|
||||
parser.quit_parsing();
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
|
||||
// Clearance the tower-approach router keeps around the tower: the avoid box is
|
||||
// inflated by this much before routing, and the inflated corners must stay on the
|
||||
// bed for a route to be generated at all.
|
||||
static constexpr float wipe_tower_routing_clearance = 2.f;
|
||||
|
||||
// BBS
|
||||
// start_pos refers to the last position before the wipe_tower.
|
||||
// end_pos refers to the wipe tower's start_pos.
|
||||
// using the print coordinate system
|
||||
Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const BoundingBox& printer_bbx) const
|
||||
Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const Polygons& bed_polygons) const
|
||||
{
|
||||
Polyline res;
|
||||
coord_t alpha = scaled(2.f); // offset distance
|
||||
coord_t alpha = scaled(wipe_tower_routing_clearance); // offset distance
|
||||
BoundingBox avoid_polygon_inner = avoid_polygon;
|
||||
avoid_polygon_inner.offset(alpha);
|
||||
coord_t width = avoid_polygon_inner.max[0] - avoid_polygon_inner.min[0];
|
||||
Polygon bed_polygon = printer_bbx.polygon();
|
||||
Vec2f v(1, 0); // the first print direction of end_pos.
|
||||
if (abs(end_pos[0] - avoid_polygon_inner.min[0]) < width / 2) v = -v; // judge whether the wipe tower's infill goes to the left or right.
|
||||
// Judge whether the avoid_polygon_inner is outside the printer_bbx.
|
||||
// Judge whether the avoid_polygon_inner is outside the bed. The real printable
|
||||
// outline is tested (not its bounding box), so on circular/custom beds corners
|
||||
// hanging off the bed are rejected.
|
||||
// If so, do nothing and just go directly to the end_pos.
|
||||
bool is_bbx_in_bed = true;
|
||||
Points avoid_points = avoid_polygon_inner.polygon().points;
|
||||
for (auto &wipe_tower_bbx_p : avoid_points) {
|
||||
if (ClipperLib::PointInPolygon(wipe_tower_bbx_p, bed_polygon.points) != 1) {
|
||||
is_bbx_in_bed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const bool is_bbx_in_bed = std::all_of(avoid_points.begin(), avoid_points.end(),
|
||||
[&bed_polygons](const Point &pt) { return contains(bed_polygons, pt, /*border_result=*/false); });
|
||||
if (!is_bbx_in_bed) {
|
||||
res.points.push_back(end_pos);
|
||||
return res;
|
||||
@@ -852,6 +891,77 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return res;
|
||||
}
|
||||
|
||||
// Type2 tower-local point -> bed frame. The rib-wall offset is tower-local, so it
|
||||
// rotates with the tower (unlike the BBL tower in append_tcr, which never rotates).
|
||||
Vec2f WipeTowerIntegration::transform_wt2_pt(const Vec2f &pt) const
|
||||
{
|
||||
const float alpha = m_wipe_tower_rotation / 180.f * float(M_PI);
|
||||
return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos;
|
||||
}
|
||||
|
||||
// Bed outline the tower-approach router plans against, in object coordinates. The real
|
||||
// outline is returned, not its bounding box, so the router's containment tests fail off
|
||||
// the bed on circular/custom shapes; the multi-nozzle narrowing lives in the accessor.
|
||||
Polygons WipeTowerIntegration::shared_printable_area(GCode &gcodegen) const
|
||||
{
|
||||
// The frame change is a pure translation, so transform the origin once.
|
||||
const Point offset = wipe_tower_point_to_object_point(gcodegen, Vec2f(m_plate_origin(0), m_plate_origin(1)));
|
||||
Polygons bed_polygons = gcodegen.m_print->get_extruder_shared_printable_polygon();
|
||||
for (Polygon &poly : bed_polygons)
|
||||
poly.translate(offset);
|
||||
return bed_polygons;
|
||||
}
|
||||
|
||||
// With skip points enabled the Type2 tower wall has an opening at each toolchange's
|
||||
// entry (tcr.start_pos): route the approach around the tower's bounding box so the
|
||||
// nozzle enters through that opening instead of dragging across the printed wall
|
||||
// (append_tcr parity). Emits only the waypoints leading up to the opening — the
|
||||
// caller still travels to start_wipe_pos itself. Returns an empty string when the
|
||||
// gap wall is off (option off or cone wall) or the approach already starts inside
|
||||
// the tower: such hops never cross the wall and must stay direct.
|
||||
std::string WipeTowerIntegration::travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const
|
||||
{
|
||||
if (!WipeTower2::use_gap_wall(gcodegen.m_config))
|
||||
return {};
|
||||
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
|
||||
// Transform tower-local corners exactly like the tcr points; a rotated tower gets a
|
||||
// conservative axis-aligned envelope from the result.
|
||||
auto tower_polygon = [&](const BoundingBoxf &bbx) {
|
||||
Polygon poly = scaled(bbx).polygon();
|
||||
for (Point &p : poly.points)
|
||||
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
|
||||
return poly;
|
||||
};
|
||||
// The avoid envelope covers the first-layer brim (and rib flare), which a travel may
|
||||
// cross freely: early-out only when the approach already starts over the tower body
|
||||
// itself, so a start between the wall and the brim edge still gets routed in through
|
||||
// the wall opening. Test the rotated polygon, not its bounding box — at angles off the
|
||||
// axes the box's corner triangles cover most of the brim ring.
|
||||
const float body_width = gcodegen.m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? m_wipe_tower_depth : m_right;
|
||||
if (tower_polygon(BoundingBoxf(Vec2d(0., 0.), Vec2d(body_width, m_wipe_tower_depth))).contains(route_start))
|
||||
return {};
|
||||
|
||||
const Polygons bed = shared_printable_area(gcodegen);
|
||||
BoundingBox avoid_bbx = get_extents(tower_polygon(m_wipe_tower_bbx));
|
||||
// The inflated corners must stay on the bed for the router to generate a route at all:
|
||||
// clamp the box against the bed shrunk by the clearance the router adds, so a tower
|
||||
// parked near the bed edge is still routed along the clamped side instead of always
|
||||
// travelling straight across the tower.
|
||||
BoundingBox clamp_bbx = get_extents(bed);
|
||||
clamp_bbx.offset(-(scaled(wipe_tower_routing_clearance) + SCALED_EPSILON));
|
||||
avoid_bbx.min = avoid_bbx.min.cwiseMax(clamp_bbx.min);
|
||||
avoid_bbx.max = avoid_bbx.max.cwiseMin(clamp_bbx.max);
|
||||
if (avoid_bbx.min.x() >= avoid_bbx.max.x() || avoid_bbx.min.y() >= avoid_bbx.max.y())
|
||||
return {};
|
||||
|
||||
Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, bed);
|
||||
std::string gcode;
|
||||
// The polyline's last point is start_wipe_pos itself — emitted by the caller.
|
||||
for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i)
|
||||
gcode += gcodegen.travel_to(travel_polyline.points[i], erMixed, "Travel to a Wipe Tower");
|
||||
return gcode;
|
||||
}
|
||||
|
||||
std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const
|
||||
{
|
||||
if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
|
||||
@@ -962,6 +1072,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
std::string change_filament_gcode = gcodegen.config().change_filament_gcode.value;
|
||||
|
||||
bool is_used_travel_avoid_perimeter = gcodegen.m_config.prime_tower_skip_points.value;
|
||||
if (is_nozzle_change && !tcr.nozzle_change_result.is_extruder_change) is_used_travel_avoid_perimeter = false;
|
||||
|
||||
// add nozzle change gcode into change filament gcode
|
||||
std::string nozzle_change_gcode_trans;
|
||||
@@ -1039,8 +1150,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
float old_retract_length = (old_filament_id != -1) ? full_config.retraction_length.get_at(old_fi) : 0;
|
||||
float new_retract_length = full_config.retraction_length.get_at(new_fi);
|
||||
float old_retract_length_toolchange = (old_filament_id != -1) ? full_config.retract_length_toolchange.get_at(old_filament_id) : 0;
|
||||
float new_retract_length_toolchange = full_config.retract_length_toolchange.get_at(new_filament_id);
|
||||
float old_retract_length_toolchange = (old_filament_id != -1) ? full_config.retract_length_toolchange.get_at(old_fi) : 0;
|
||||
float new_retract_length_toolchange = full_config.retract_length_toolchange.get_at(new_fi);
|
||||
int old_filament_temp = (old_filament_id != -1) ? (gcodegen.on_first_layer()? full_config.nozzle_temperature_initial_layer.get_at(old_fi) : full_config.nozzle_temperature.get_at(old_fi)) : 210;
|
||||
int new_filament_temp = gcodegen.on_first_layer() ? full_config.nozzle_temperature_initial_layer.get_at(new_fi) : full_config.nozzle_temperature.get_at(new_fi);
|
||||
Vec3d nozzle_pos = gcode_writer.get_position();
|
||||
@@ -1224,24 +1335,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
Vec2f gcode_last_pos2d{gcode_last_pos[0], gcode_last_pos[1]};
|
||||
Point gcode_last_pos2d_object = gcodegen.gcode_to_point(gcode_last_pos2d.cast<double>() + plate_origin_2d.cast<double>());
|
||||
Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, tool_change_start_pos + plate_origin_2d);
|
||||
BoundingBox avoid_bbx, printer_bbx;
|
||||
{
|
||||
// set printer_bbx
|
||||
// Multi-nozzle: clamp the avoid-perimeter travel bounds to the region every
|
||||
// extruder can reach (get_extruder_shared_printable_polygon) instead of the full
|
||||
// bed. Gated on the multi-nozzle predicate so H2D and every existing single/dual
|
||||
// printer keep the historic full-printable_area routing byte-identical.
|
||||
if (is_multi_nozzle_printer(gcodegen.m_config)) {
|
||||
printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon());
|
||||
printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.min) + plate_origin_2d);
|
||||
printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.max) + plate_origin_2d);
|
||||
} else {
|
||||
Pointfs bed_pointsf = gcodegen.m_config.printable_area.values;
|
||||
Points bed_points;
|
||||
for (auto p : bed_pointsf) { bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast<float>() + plate_origin_2d)); }
|
||||
printer_bbx = BoundingBox(bed_points);
|
||||
}
|
||||
}
|
||||
BoundingBox avoid_bbx;
|
||||
{
|
||||
// set avoid_bbx
|
||||
avoid_bbx = scaled(m_wipe_tower_bbx);
|
||||
@@ -1253,7 +1347,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
avoid_bbx = BoundingBox(avoid_points.points);
|
||||
}
|
||||
std::string travel_to_wipe_tower_gcode;
|
||||
Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, printer_bbx);
|
||||
Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, shared_printable_area(gcodegen));
|
||||
|
||||
for (size_t i = 0; i < travel_polyline.points.size(); ++i) {
|
||||
const auto &p = travel_polyline.points[i];
|
||||
@@ -1271,20 +1365,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
}
|
||||
|
||||
// do unretract after setting current extruder_id
|
||||
// PETG filaments on a device with a filament switcher get a small (2 mm) pre-extrusion
|
||||
// before the tool change. has_filament_switcher is a develop-only key read defensively from the
|
||||
// full config (Orca does not carry it as a static PrintConfig member — same convention as
|
||||
// enable_filament_dynamic_map); no shipping profile sets it (grep resources/profiles = 0), so
|
||||
// is_petg_pre_extrusion is always false -> extra_unretract stays 0 -> byte-identical to the plain
|
||||
// unretract() fleet-wide. The tower-interface contact pre-extrusion length (the
|
||||
// is_contact_pre_extrusion branch) is NOT applied here; it is only computed as the guard used to
|
||||
// give the contact path priority over PETG.
|
||||
// BBS pattern: the wipe tower shifts the toolchange start position outward for the
|
||||
// tower-interface (contact) pre-extrusion and for the PETG-with-filament-switcher case;
|
||||
// the pre-extrusion material itself is laid down here as extra unretract on the approach.
|
||||
// has_filament_switcher is a develop-only key read defensively from the full config (Orca
|
||||
// does not carry it as a static PrintConfig member — same convention as
|
||||
// enable_filament_dynamic_map); no shipping profile sets it, so is_petg_pre_extrusion is
|
||||
// always false fleet-wide.
|
||||
const ConfigOptionBool* has_filament_switcher_opt = gcodegen.m_print->full_print_config().option<ConfigOptionBool>("has_filament_switcher");
|
||||
bool is_contact_pre_extrusion = tcr.is_contact && gcodegen.m_config.enable_tower_interface_features;
|
||||
bool is_petg_pre_extrusion = !is_contact_pre_extrusion
|
||||
&& gcodegen.config().filament_type.get_at(tcr.new_tool) == "PETG"
|
||||
&& has_filament_switcher_opt && has_filament_switcher_opt->value;
|
||||
float extra_unretract = is_petg_pre_extrusion ? 2.f : 0.f;
|
||||
float extra_unretract = 0.f;
|
||||
if (is_contact_pre_extrusion)
|
||||
extra_unretract = gcodegen.m_config.filament_tower_interface_pre_extrusion_length.get_at(tcr.new_tool);
|
||||
else if (is_petg_pre_extrusion)
|
||||
extra_unretract = 2.f;
|
||||
std::string toolchange_unretract_str = (extra_unretract > 0.f) ? gcodegen.unretract(extra_unretract) : gcodegen.unretract();
|
||||
check_add_eol(toolchange_unretract_str);
|
||||
|
||||
@@ -1382,20 +1479,16 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
// We want to rotate and shift all extrusions (gcode postprocessing) and starting and ending position
|
||||
float alpha = m_wipe_tower_rotation / 180.f * float(M_PI);
|
||||
|
||||
auto transform_wt_pt = [&alpha, this](const Vec2f &pt) -> Vec2f {
|
||||
Vec2f out = Eigen::Rotation2Df(alpha) * pt;
|
||||
out += m_wipe_tower_pos;
|
||||
return out;
|
||||
};
|
||||
|
||||
// Priming lines are absolute bed moves; everything else is tower-local
|
||||
// (transform_wt2_pt).
|
||||
Vec2f start_pos = tcr.start_pos;
|
||||
Vec2f end_pos = tcr.end_pos;
|
||||
if (!tcr.priming) {
|
||||
start_pos = transform_wt_pt(start_pos);
|
||||
end_pos = transform_wt_pt(end_pos);
|
||||
start_pos = transform_wt2_pt(start_pos);
|
||||
end_pos = transform_wt2_pt(end_pos);
|
||||
}
|
||||
|
||||
Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : m_wipe_tower_pos;
|
||||
Vec2f wipe_tower_offset = tcr.priming ? Vec2f::Zero() : Vec2f(m_wipe_tower_pos + Eigen::Rotation2Df(alpha) * m_rib_offset);
|
||||
float wipe_tower_rotation = tcr.priming ? 0.f : alpha;
|
||||
Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
|
||||
|
||||
@@ -1425,16 +1518,34 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|| is_ramming
|
||||
|| tool_change_on_wipe_tower);
|
||||
|
||||
if (should_travel_to_tower || gcodegen.m_need_change_layer_lift_z) {
|
||||
const Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d);
|
||||
const bool travel_to_tower_now = should_travel_to_tower || gcodegen.m_need_change_layer_lift_z;
|
||||
if (travel_to_tower_now) {
|
||||
// FIXME: It would be better if the wipe tower set the force_travel flag for all toolchanges,
|
||||
// then we could simplify the condition and make it more readable.
|
||||
gcode += gcodegen.retract();
|
||||
|
||||
// Orca: pass the configured lift type, as append_tcr does above. lazy_lift() keeps
|
||||
// the first type it is given, so the NormalLift default would pin this hop to a
|
||||
// standing move. Slope and spiral both need a known head position.
|
||||
LiftType lift_type = LiftType::NormalLift;
|
||||
if (gcodegen.writer().filament() != nullptr && gcodegen.writer().is_current_position_clear()) {
|
||||
ZHopType z_hop_type = ZHopType(gcodegen.config().z_hop_types.get_at(
|
||||
gcodegen.get_filament_config_index((int) gcodegen.writer().filament()->id())));
|
||||
if (z_hop_type == ZHopType::zhtAuto)
|
||||
z_hop_type = ZHopType::zhtSpiral;
|
||||
lift_type = gcodegen.to_lift_type(z_hop_type);
|
||||
}
|
||||
gcode += gcodegen.retract(false, false, lift_type);
|
||||
gcodegen.m_avoid_crossing_perimeters.use_external_mp_once();
|
||||
gcode += gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, start_pos + plate_origin_2d), erMixed, "Travel to a Wipe Tower");
|
||||
if (!tcr.priming && gcodegen.last_pos_defined())
|
||||
gcode += travel_to_tower_gap(gcodegen, gcodegen.last_pos(), start_wipe_pos);
|
||||
gcode += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower");
|
||||
gcode += gcodegen.unretract();
|
||||
} else {
|
||||
// When this is multiextruder printer without any ramming, we can just change
|
||||
// the tool without travelling to the tower.
|
||||
// the tool without travelling to the tower. The tower entry travel then lives
|
||||
// inside the tcr gcode; with skip points on it is rerouted below, once the
|
||||
// toolchange gcode (and the head position it ends at) is known.
|
||||
}
|
||||
|
||||
if (will_go_down) {
|
||||
@@ -1456,7 +1567,38 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id);
|
||||
toolchange_temp_override = interface_temp;
|
||||
}
|
||||
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override); // TODO: toolchange_z vs print_z
|
||||
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override,
|
||||
WipeTower2::wait_for_temp_enabled(gcodegen.m_config)); // TODO: toolchange_z vs print_z
|
||||
if (!travel_to_tower_now && !tcr.priming && WipeTower2::use_gap_wall(gcodegen.m_config)) {
|
||||
// The tool changed in place (multi-tool printer without ramming), so the
|
||||
// tower entry is the tcr's own positioning move — a straight line across
|
||||
// the printed wall. Route it around the tower and in through the wall
|
||||
// opening instead, riding at the end of the change_filament_gcode
|
||||
// substitution so the generator's positioning move degrades to a
|
||||
// zero-length one (append_tcr parity: travel after the filament change,
|
||||
// retracted, with the new filament).
|
||||
Vec3f last_gcode_pos = gcodegen.writer().get_position().cast<float>();
|
||||
Point route_start;
|
||||
bool have_start = false;
|
||||
if (GCodeProcessor::get_last_position_from_gcode(toolchange_gcode_str, last_gcode_pos)) {
|
||||
// A custom change_filament_gcode may have moved the head (tool docks
|
||||
// etc.); recover the real position from the emitted gcode.
|
||||
route_start = gcodegen.gcode_to_point(Vec2d(last_gcode_pos.x(), last_gcode_pos.y()) + plate_origin_2d.cast<double>());
|
||||
have_start = true;
|
||||
} else if (gcodegen.last_pos_defined()) {
|
||||
route_start = gcodegen.last_pos();
|
||||
have_start = true;
|
||||
}
|
||||
if (have_start) {
|
||||
gcodegen.set_last_pos(route_start);
|
||||
gcodegen.m_avoid_crossing_perimeters.use_external_mp_once();
|
||||
std::string travel = travel_to_tower_gap(gcodegen, route_start, start_wipe_pos);
|
||||
travel += gcodegen.travel_to(start_wipe_pos, erMixed, "Travel to a Wipe Tower");
|
||||
check_add_eol(travel);
|
||||
toolchange_gcode_str += travel;
|
||||
gcodegen.set_last_pos(start_wipe_pos);
|
||||
}
|
||||
}
|
||||
if (gcodegen.config().enable_prime_tower) {
|
||||
deretraction_str += gcodegen.writer().travel_to_z(z, "Force restore layer Z", true);
|
||||
Vec3d position{gcodegen.writer().get_position()};
|
||||
@@ -1577,7 +1719,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
std::string trimmed = line;
|
||||
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
|
||||
bool skip_line = false;
|
||||
if (boost::starts_with(trimmed, "M109")) {
|
||||
if (boost::starts_with(trimmed, "M109") && trimmed.find(WipeTower2::wait_for_temp_tag()) == std::string::npos) {
|
||||
bool matches_extruder = true;
|
||||
if (trimmed.find('T') != std::string::npos)
|
||||
matches_extruder = trimmed.find(t_token) != std::string::npos;
|
||||
@@ -1642,7 +1784,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
// Prepare a future wipe.
|
||||
gcodegen.m_wipe.reset_path();
|
||||
for (const Vec2f& wipe_pt : tcr.wipe_path)
|
||||
gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(wipe_pt) + plate_origin_2d));
|
||||
gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(wipe_pt) + plate_origin_2d));
|
||||
}
|
||||
|
||||
// Let the planner know we are traveling between objects.
|
||||
@@ -2756,6 +2898,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled,
|
||||
print.get_layered_nozzle_group_result());
|
||||
const bool is_bbl_printers = print.is_BBL_printer();
|
||||
const bool skip_config_block = print.config().gcode_skip_config_block;
|
||||
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
|
||||
m_calib_config.clear();
|
||||
// resets analyzer's tracking data
|
||||
@@ -2768,6 +2911,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
m_role_based_fan_marker_layer.fill(-1);
|
||||
|
||||
m_fan_mover.release();
|
||||
m_ordering_cache.clear();
|
||||
|
||||
m_writer.set_is_bbl_machine(is_bbl_printers);
|
||||
|
||||
@@ -2930,7 +3074,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
// as configuration key / value pairs to be parsable by older versions of
|
||||
// PrusaSlicer G-code viewer.
|
||||
{
|
||||
if (is_bbl_printers) {
|
||||
if (is_bbl_printers && !skip_config_block) {
|
||||
file.write("; CONFIG_BLOCK_START\n");
|
||||
std::string full_config;
|
||||
append_full_config(print, full_config);
|
||||
@@ -3088,11 +3232,20 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
// In non-sequential print, the printing extruders may have been modified by the extruder switches stored in Model::custom_gcode_per_print_z.
|
||||
// Therefore initialize the printing extruders from there.
|
||||
this->set_extruders(tool_ordering.all_extruders());
|
||||
print_object_instances_ordering =
|
||||
// By default, order object instances using a nearest neighbor search.
|
||||
print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print)
|
||||
print_object_instances_ordering =
|
||||
// By default, order object instances using nearest-neighbor chaining plus
|
||||
// 2-opt and crossing-removal post-processing.
|
||||
(print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print)
|
||||
// Snake: serpentine row traversal + 2-opt
|
||||
: (print.config().print_order == PrintOrder::Snake ? chain_print_object_instances_snake(print)
|
||||
// Best of all: run every strategy, pick the shortest total path
|
||||
: (print.config().print_order == PrintOrder::BestOfStrategies ? chain_print_object_instances_best_of(print)
|
||||
// Otherwise same order as the object list
|
||||
: sort_object_instances_by_model_order(print);
|
||||
: sort_object_instances_by_model_order(print))));
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
if (initial_extruder_id == (unsigned int)-1) {
|
||||
// Nothing to print!
|
||||
@@ -3948,23 +4101,25 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder)
|
||||
.c_str());
|
||||
file.write("\n");
|
||||
file.write("; CONFIG_BLOCK_START\n");
|
||||
std::string full_config;
|
||||
append_full_config(print, full_config);
|
||||
if (!full_config.empty())
|
||||
file.write(full_config);
|
||||
if (!skip_config_block) {
|
||||
file.write("; CONFIG_BLOCK_START\n");
|
||||
std::string full_config;
|
||||
append_full_config(print, full_config);
|
||||
if (!full_config.empty())
|
||||
file.write(full_config);
|
||||
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0);
|
||||
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
// SoftFever: write compatiple info
|
||||
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
|
||||
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
|
||||
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
|
||||
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
|
||||
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
|
||||
|
||||
//SF TODO
|
||||
// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0);
|
||||
|
||||
file.write("; CONFIG_BLOCK_END\n\n");
|
||||
} // !skip_config_block
|
||||
|
||||
}
|
||||
file.write("\n");
|
||||
@@ -4335,6 +4490,11 @@ PlaceholderParserIntegration &ppi = m_placeholder_parser_integration;
|
||||
ppi.update_from_gcodewriter(m_writer);
|
||||
std::string output = ppi.parser.process(templ, current_filament_id, config_override, &ppi.output_config, &ppi.context);
|
||||
ppi.validate_output_vector_variables();
|
||||
const CustomGCodeMotionStateChanges motion_state_changes = custom_gcode_motion_state_changes(output);
|
||||
if (motion_state_changes.acceleration)
|
||||
m_writer.invalidate_acceleration();
|
||||
if (motion_state_changes.jerk)
|
||||
m_writer.invalidate_jerk();
|
||||
|
||||
if (const std::vector<double> &pos = ppi.opt_position->values; ppi.position != pos) {
|
||||
// Update G-code writer.
|
||||
@@ -5365,7 +5525,7 @@ LayerResult GCode::process_layer(
|
||||
// add tag for processor
|
||||
gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n";
|
||||
// export layer z
|
||||
char buf[64];
|
||||
char buf[80];
|
||||
sprintf(buf, print.is_BBL_printer() ? "; Z_HEIGHT: %g\n" : ";Z:%g\n", print_z);
|
||||
gcode += buf;
|
||||
// export layer height
|
||||
@@ -5478,7 +5638,9 @@ LayerResult GCode::process_layer(
|
||||
//Calibration Layer-specific GCode
|
||||
switch (print.calib_mode()) {
|
||||
case CalibMode::Calib_PA_Tower: {
|
||||
gcode += writer().set_pressure_advance(print.calib_params().start + static_cast<int>(print_z) * print.calib_params().step);
|
||||
gcode += writer().set_pressure_advance(this->interpolate_value_across_layers(static_cast<float>(print.calib_params().start),
|
||||
static_cast<float>(print.calib_params().end),
|
||||
static_cast<float>(print.calib_params().step)));
|
||||
break;
|
||||
}
|
||||
case CalibMode::Calib_Temp_Tower: {
|
||||
@@ -5486,7 +5648,12 @@ LayerResult GCode::process_layer(
|
||||
break;
|
||||
}
|
||||
case CalibMode::Calib_VFA_Tower: {
|
||||
auto _speed = print.calib_params().start + std::floor(print_z / 5.0) * print.calib_params().step;
|
||||
// Step the outer wall speed from start to end across the tower's layers. Plater::calib_VFA sizes the
|
||||
// geometry so each speed step spans one visual block (a fixed number of layers), so the layer-based
|
||||
// stepping stays aligned with the blocks regardless of nozzle size / layer height.
|
||||
float _speed = this->interpolate_value_across_layers(static_cast<float>(print.calib_params().start),
|
||||
static_cast<float>(print.calib_params().end),
|
||||
static_cast<float>(print.calib_params().step));
|
||||
m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable({std::round(_speed)}));
|
||||
break;
|
||||
}
|
||||
@@ -5640,10 +5807,17 @@ LayerResult GCode::process_layer(
|
||||
for (const auto &layer_to_print : layers) {
|
||||
if (layer_to_print.object_layer) {
|
||||
const auto& regions = layer_to_print.object_layer->regions();
|
||||
const bool enable_overhang_speed = std::any_of(regions.begin(), regions.end(), [this](const LayerRegion* r) {
|
||||
const bool has_extrusions = std::any_of(regions.begin(), regions.end(), [](const LayerRegion* r) {
|
||||
return r->has_extrusions();
|
||||
});
|
||||
const bool enable_overhang_speed = std::any_of(regions.begin(), regions.end(), [this](const LayerRegion* r) {
|
||||
return r->has_extrusions() && r->region().config().enable_overhang_speed.get_at(get_nozzle_config_index(m_writer.filament()->id()));
|
||||
});
|
||||
if (enable_overhang_speed) {
|
||||
const bool enable_overhang_fan = m_enable_cooling_markers && has_extrusions &&
|
||||
std::any_of(m_config.enable_overhang_bridge_fan.values.begin(),
|
||||
m_config.enable_overhang_bridge_fan.values.end(),
|
||||
[](unsigned char value) { return value != 0; });
|
||||
if (enable_overhang_speed || enable_overhang_fan) {
|
||||
m_extrusion_quality_estimator.prepare_for_new_layer(layer_to_print.original_object,
|
||||
layer_to_print.object_layer);
|
||||
}
|
||||
@@ -5911,41 +6085,128 @@ LayerResult GCode::process_layer(
|
||||
if (m_farthest_point_timelapse.enabled)
|
||||
compute_farthest_point(layers, most_used_extruder, support_filaments);
|
||||
|
||||
std::map<unsigned int, std::vector<InstanceToPrint>> filament_to_print_instances;
|
||||
// Per filament: instances to print, and the visit sequence over them. Island-level ordering
|
||||
// may visit an instance more than once per layer; otherwise one visit per instance.
|
||||
std::map<unsigned int, std::pair<std::vector<InstanceToPrint>, std::vector<InstanceVisit>>> filament_to_print_instances;
|
||||
{
|
||||
// Order individual islands rather than whole instances. Off for by-object sequencing,
|
||||
// sequential printing, and the explicit AsObjectList order, which tour whole instances.
|
||||
const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject &&
|
||||
single_object_instance_idx == size_t(-1) &&
|
||||
print.config().print_order != PrintOrder::AsObjectList;
|
||||
for (unsigned int filament_id : layer_tools.extruders) {
|
||||
auto objects_by_extruder_it = by_extruder.find(filament_id);
|
||||
if (objects_by_extruder_it == by_extruder.end()) continue;
|
||||
|
||||
auto &filament_plan = filament_to_print_instances[filament_id];
|
||||
|
||||
if (!island_level_ordering) {
|
||||
// One visit per instance, printing all of its islands.
|
||||
filament_plan.first = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
|
||||
filament_plan.second.reserve(filament_plan.first.size());
|
||||
for (size_t i = 0; i < filament_plan.first.size(); ++i)
|
||||
filament_plan.second.push_back({i, {}, true});
|
||||
continue;
|
||||
}
|
||||
|
||||
int plate_idx = print.get_plate_index();
|
||||
Point wt_pos(print.config().wipe_tower_x.get_at(plate_idx), print.config().wipe_tower_y.get_at(plate_idx));
|
||||
|
||||
// Build the instances and one tour node per non-empty island (a single node for
|
||||
// instances without chainable islands). Positions quantized to 1 mm so small
|
||||
// centroid drift between layers still hits the tour cache below.
|
||||
std::vector<GCode::ObjectByExtruder> &objects_by_extruder = objects_by_extruder_it->second;
|
||||
std::vector<const PrintObject *> print_objects;
|
||||
for (int obj_idx = 0; obj_idx < objects_by_extruder.size(); obj_idx++) {
|
||||
auto &object_by_extruder = objects_by_extruder[obj_idx];
|
||||
std::vector<InstanceToPrint> &instances = filament_plan.first;
|
||||
std::vector<IslandOrderNode> nodes;
|
||||
std::vector<size_t> node_instances;
|
||||
auto quantize_to_mm = [](const Point &pt) -> Point {
|
||||
const coord_t grid = coord_t(scale_(1.));
|
||||
// Round to the nearest 1 mm symmetrically (integer division truncates toward
|
||||
// zero, which would make the bucket straddling the origin twice as wide).
|
||||
auto q = [grid](coord_t v) -> coord_t {
|
||||
return ((v >= 0 ? v + grid / 2 : v - grid / 2) / grid) * grid;
|
||||
};
|
||||
return Point(q(pt.x()), q(pt.y()));
|
||||
};
|
||||
for (ObjectByExtruder &object_by_extruder : objects_by_extruder) {
|
||||
if (object_by_extruder.islands.empty() && (object_by_extruder.support == nullptr || object_by_extruder.support->empty())) continue;
|
||||
|
||||
print_objects.push_back(print.get_object(obj_idx));
|
||||
const size_t layer_id = &object_by_extruder - objects_by_extruder.data();
|
||||
const PrintObject *print_object = layers[layer_id].original_object;
|
||||
if (print_object == nullptr)
|
||||
continue;
|
||||
const Layer *obj_layer = layers[layer_id].object_layer;
|
||||
std::vector<ObjectByExtruder::Island> &islands = object_by_extruder.islands;
|
||||
const bool islands_chainable = obj_layer != nullptr && islands.size() == obj_layer->lslices.size() + 1;
|
||||
for (size_t instance_id = 0; instance_id < print_object->instances().size(); ++instance_id) {
|
||||
const size_t instance_idx = instances.size();
|
||||
instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id,
|
||||
print_object->instances()[instance_id].model_instance->get_labeled_id());
|
||||
const Point &shift = print_object->instances()[instance_id].shift;
|
||||
const size_t first_node = nodes.size();
|
||||
if (islands_chainable)
|
||||
for (size_t i = 0; i + 1 < islands.size(); ++i)
|
||||
if (!islands[i].by_region.empty()) {
|
||||
nodes.push_back({print_object->id(), instance_id, i,
|
||||
quantize_to_mm(obj_layer->lslices[i].contour.centroid() + shift)});
|
||||
node_instances.emplace_back(instance_idx);
|
||||
}
|
||||
if (nodes.size() == first_node) {
|
||||
// No chainable islands: tour the whole instance as one stop.
|
||||
nodes.push_back({print_object->id(), instance_id, size_t(-1), quantize_to_mm(shift)});
|
||||
node_instances.emplace_back(instance_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<const PrintInstance *> new_ordering = chain_print_object_instances(print_objects, &wt_pos);
|
||||
std::reverse(new_ordering.begin(), new_ordering.end());
|
||||
// Reuse the cached tour while this filament's island layout is unchanged.
|
||||
auto &cache_entry = m_ordering_cache[filament_id];
|
||||
if (!(cache_entry.first == nodes)) {
|
||||
cache_entry.first = nodes;
|
||||
Points node_points;
|
||||
node_points.reserve(nodes.size());
|
||||
for (const IslandOrderNode &node : nodes)
|
||||
node_points.emplace_back(node.pos);
|
||||
std::vector<size_t> tour = order_points_with_strategy(node_points, print.config().print_order, &wt_pos);
|
||||
// Chained starting near the wipe tower, reversed so the layer ends near it.
|
||||
std::reverse(tour.begin(), tour.end());
|
||||
|
||||
if (print.config().print_sequence == PrintSequence::ByObject) {
|
||||
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
|
||||
} else {
|
||||
|
||||
// PrintSequence::ByLayer to use global ordering ( per object ordering ) if intra-layer order PrintOrder::AsObjectList is specified while keeping behaviour of PrintSequence::ByLayer
|
||||
const std::vector<const PrintInstance*>* ordering_for_filament = (print.config().print_order == PrintOrder::AsObjectList && ordering != nullptr) ? ordering: &new_ordering;
|
||||
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering_for_filament, single_object_instance_idx);
|
||||
// Group consecutive tour stops of the same instance into visits.
|
||||
std::vector<InstanceVisit> visits;
|
||||
std::vector<bool> instance_seen(instances.size(), false);
|
||||
std::vector<int> last_visit_of_instance(instances.size(), -1);
|
||||
for (size_t node_idx : tour) {
|
||||
const size_t instance_idx = node_instances[node_idx];
|
||||
if (visits.empty() || visits.back().instance_idx != instance_idx) {
|
||||
visits.push_back({instance_idx, {}, !instance_seen[instance_idx]});
|
||||
instance_seen[instance_idx] = true;
|
||||
}
|
||||
if (nodes[node_idx].island_idx != size_t(-1))
|
||||
visits.back().islands.emplace_back(nodes[node_idx].island_idx);
|
||||
last_visit_of_instance[instance_idx] = int(visits.size()) - 1;
|
||||
}
|
||||
// The trailing catch-all island has no geometry to chain by; append it to the
|
||||
// instance's last visit.
|
||||
for (size_t i = 0; i < instances.size(); ++i) {
|
||||
if (last_visit_of_instance[i] < 0)
|
||||
continue;
|
||||
InstanceVisit &last_visit = visits[size_t(last_visit_of_instance[i])];
|
||||
if (last_visit.islands.empty())
|
||||
// A visit without explicit islands already prints everything.
|
||||
continue;
|
||||
std::vector<ObjectByExtruder::Island> &islands = instances[i].object_by_extruder.islands;
|
||||
if (!islands.back().by_region.empty())
|
||||
last_visit.islands.emplace_back(islands.size() - 1);
|
||||
}
|
||||
cache_entry.second = std::move(visits);
|
||||
}
|
||||
filament_plan.second = cache_entry.second;
|
||||
}
|
||||
}
|
||||
|
||||
std::set<size_t> layer_object_label_ids;
|
||||
for (auto iter = filament_to_print_instances.begin(); iter != filament_to_print_instances.end(); ++iter) {
|
||||
for (const InstanceToPrint &instance : iter->second) {
|
||||
for (const InstanceToPrint &instance : iter->second.first) {
|
||||
layer_object_label_ids.insert(instance.label_object_id);
|
||||
}
|
||||
}
|
||||
@@ -6015,7 +6276,7 @@ LayerResult GCode::process_layer(
|
||||
|
||||
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
|
||||
std::vector<size_t> filament_instances_id;
|
||||
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id]) filament_instances_id.emplace_back(instance.label_object_id);
|
||||
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id);
|
||||
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
|
||||
}
|
||||
|
||||
@@ -6096,7 +6357,9 @@ LayerResult GCode::process_layer(
|
||||
if (layer_tools.has_wipe_tower && m_wipe_tower)
|
||||
m_last_processor_extrusion_role = erWipeTower;
|
||||
|
||||
std::vector<InstanceToPrint> &instances_to_print = filament_to_print_instances[extruder_id];
|
||||
auto &filament_plan = filament_to_print_instances[extruder_id];
|
||||
std::vector<InstanceToPrint> &instances_to_print = filament_plan.first;
|
||||
const std::vector<InstanceVisit> &instance_visits = filament_plan.second;
|
||||
|
||||
// We are almost ready to print. However, we must go through all the objects twice to print the overridden extrusions first (infill/perimeter wiping feature):
|
||||
std::vector<ObjectByExtruder::Island::Region> by_region_per_copy_cache;
|
||||
@@ -6104,10 +6367,11 @@ LayerResult GCode::process_layer(
|
||||
if (is_anything_overridden && print_wipe_extrusions == 0)
|
||||
gcode+="; PURGING FINISHED\n";
|
||||
|
||||
for (InstanceToPrint &instance_to_print : instances_to_print) {
|
||||
for (const InstanceVisit &visit : instance_visits) {
|
||||
InstanceToPrint &instance_to_print = instances_to_print[visit.instance_idx];
|
||||
const auto& inst = instance_to_print.print_object.instances()[instance_to_print.instance_id];
|
||||
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
|
||||
if (print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
|
||||
if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
|
||||
gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id);
|
||||
gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer);
|
||||
}
|
||||
@@ -6160,7 +6424,7 @@ LayerResult GCode::process_layer(
|
||||
m_avoid_crossing_perimeters.use_external_mp_once();
|
||||
m_last_obj_copy = this_object_copy;
|
||||
this->set_origin(unscale(offset));
|
||||
if (instance_to_print.object_by_extruder.support != nullptr) {
|
||||
if (visit.first_visit && instance_to_print.object_by_extruder.support != nullptr) {
|
||||
m_layer = layers[instance_to_print.layer_id].support_layer;
|
||||
m_object_layer_over_raft = false;
|
||||
|
||||
@@ -6194,9 +6458,42 @@ LayerResult GCode::process_layer(
|
||||
m_layer = layer_to_print.layer();
|
||||
m_object_layer_over_raft = object_layer_over_raft;
|
||||
}
|
||||
//FIXME order islands?
|
||||
// Sequential tool path ordering of multiple parts within the same object, aka. perimeter tracking (#5511)
|
||||
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
|
||||
// Island print order. Use the islands the tour assigned to this visit; if none,
|
||||
// chain all islands nearest-neighbor from the current nozzle position (last_pos(),
|
||||
// in this instance's frame after set_origin() above). Empty islands are skipped;
|
||||
// the trailing catch-all island has no centroid to chain by and always goes last.
|
||||
std::vector<ObjectByExtruder::Island> &islands = instance_to_print.object_by_extruder.islands;
|
||||
std::vector<size_t> island_order = visit.islands;
|
||||
if (island_order.empty()) {
|
||||
island_order.reserve(islands.size());
|
||||
if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) {
|
||||
for (size_t i = 0; i + 1 < islands.size(); ++i)
|
||||
if (!islands[i].by_region.empty())
|
||||
island_order.emplace_back(i);
|
||||
if (island_order.size() > 1) {
|
||||
Points island_centroids;
|
||||
island_centroids.reserve(island_order.size());
|
||||
for (size_t i : island_order)
|
||||
island_centroids.emplace_back(layer_to_print.object_layer->lslices[i].contour.centroid());
|
||||
const Point start_near = this->last_pos();
|
||||
std::vector<size_t> chain = chain_points(island_centroids, this->last_pos_defined() ? &start_near : nullptr);
|
||||
std::vector<size_t> ordered;
|
||||
ordered.reserve(island_order.size());
|
||||
for (size_t k : chain)
|
||||
ordered.emplace_back(island_order[k]);
|
||||
island_order = std::move(ordered);
|
||||
}
|
||||
if (!islands.back().by_region.empty())
|
||||
island_order.emplace_back(islands.size() - 1);
|
||||
} else {
|
||||
// Unexpected islands layout, keep the stored order.
|
||||
for (size_t i = 0; i < islands.size(); ++i)
|
||||
island_order.emplace_back(i);
|
||||
}
|
||||
}
|
||||
for (size_t island_idx : island_order) {
|
||||
ObjectByExtruder::Island &island = islands[island_idx];
|
||||
const auto& by_region_specific = is_anything_overridden ? island.by_region_per_copy(by_region_per_copy_cache, static_cast<unsigned int>(instance_to_print.instance_id), extruder_id, print_wipe_extrusions != 0) : island.by_region;
|
||||
// When starting a new object, use the external motion planner for the first travel move.
|
||||
const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift;
|
||||
@@ -6429,6 +6726,7 @@ void GCode::append_full_config(const Print &print, std::string &str)
|
||||
"farthest_point_timelapse"sv,
|
||||
"compatible_printers"sv,
|
||||
"compatible_prints"sv,
|
||||
"filament_colour_type"sv,
|
||||
"print_host"sv,
|
||||
"print_host_webui"sv,
|
||||
"printhost_apikey"sv,
|
||||
@@ -7353,8 +7651,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
if (sloped) {
|
||||
speed = std::min(speed, m_config.scarf_joint_speed.get_abs_value(speed));
|
||||
}
|
||||
}
|
||||
else if(path.role() == erInternalBridgeInfill) {
|
||||
} else if(path.role() == erInternalBridgeInfill) {
|
||||
speed = m_config.get_abs_value_at("internal_bridge_speed", get_nozzle_config_index(m_writer.filament()->id()));
|
||||
} else if (path.role() == erOverhangPerimeter || path.role() == erSupportTransition || path.role() == erBridgeInfill) {
|
||||
speed = NOZZLE_CONFIG(bridge_speed);
|
||||
@@ -7365,7 +7662,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
} else if (path.role() == erTopSolidInfill) {
|
||||
speed = NOZZLE_CONFIG(top_surface_speed);
|
||||
} else if (path.role() == erIroning) {
|
||||
speed = m_config.get_abs_value("ironing_speed");
|
||||
const size_t filament_idx = get_filament_config_index(m_writer.filament()->id());
|
||||
speed = m_config.filament_ironing_speed.is_nil(filament_idx)
|
||||
? m_config.get_abs_value("ironing_speed")
|
||||
: m_config.filament_ironing_speed.get_at(filament_idx);
|
||||
} else if (path.role() == erBottomSurface) {
|
||||
speed = NOZZLE_CONFIG(initial_layer_infill_speed);
|
||||
} else if (path.role() == erGapFill) {
|
||||
@@ -7482,7 +7782,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
bool variable_speed = false;
|
||||
std::vector<ProcessedPoint> new_points {};
|
||||
|
||||
if (NOZZLE_CONFIG(enable_overhang_speed) && !this->on_first_layer() && !object_layer_over_raft() &&
|
||||
const bool need_overhang_detection = NOZZLE_CONFIG(enable_overhang_speed) ||
|
||||
(FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers);
|
||||
|
||||
if (need_overhang_detection && !this->on_first_layer() && !object_layer_over_raft() &&
|
||||
(is_bridge(path.role()) || is_perimeter(path.role()))) {
|
||||
bool is_external = is_external_perimeter(path.role());
|
||||
double ref_speed = is_external ? NOZZLE_CONFIG(outer_wall_speed) : NOZZLE_CONFIG(inner_wall_speed);
|
||||
@@ -7541,6 +7844,11 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
|
||||
}
|
||||
variable_speed = std::any_of(new_points.begin(), new_points.end(),
|
||||
[speed](const ProcessedPoint &p) { return fabs(double(p.speed) - speed) > 1; }); // Ignore small speed variations (under 1mm/sec)
|
||||
if (!NOZZLE_CONFIG(enable_overhang_speed) && FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers) {
|
||||
for (ProcessedPoint &point : new_points)
|
||||
point.speed = speed;
|
||||
variable_speed = new_points.size() > 1;
|
||||
}
|
||||
}
|
||||
|
||||
double F = speed * 60; // convert mm/sec to mm/min
|
||||
@@ -8153,29 +8461,22 @@ std::string GCode::extrusion_role_to_string_for_parser(const ExtrusionRole & rol
|
||||
}
|
||||
|
||||
// Calculate the interpolated value for the current layer between start_value and end_value.
|
||||
// Step will create equal layers steps from first to last value.
|
||||
// Step > 0 splits the range into equal-width bands from first to last value (both inclusive).
|
||||
// Step = 0 means gradual interpolation finishing at last value.
|
||||
float GCode::interpolate_value_across_layers(float start_value, float end_value, float step) const
|
||||
{
|
||||
if (m_layer_index <= 1) {
|
||||
return start_value;
|
||||
}
|
||||
else {
|
||||
bool use_steps = step > 0.f;
|
||||
if (use_steps) {
|
||||
if (start_value > end_value) {
|
||||
start_value += step;
|
||||
} else {
|
||||
end_value += step;
|
||||
}
|
||||
}
|
||||
float ratio = m_layer_index / (m_layer_count - 1.f);
|
||||
float value = start_value + ratio * (end_value - start_value);
|
||||
if (use_steps) {
|
||||
value = trunc(value / step) * step;
|
||||
}
|
||||
return value;
|
||||
const float ratio = m_layer_index / (m_layer_count - 1.f);
|
||||
if (step > 0.f) {
|
||||
// Discrete equal-width bands. band is clamped to the last band so the result can't overshoot the range:
|
||||
// at the top layer ratio * n_bands == n_bands, which would otherwise index one band past the end.
|
||||
const int n_bands = std::lround(std::abs(end_value - start_value) / step) + 1;
|
||||
const int band = std::min(n_bands - 1, static_cast<int>(ratio * n_bands));
|
||||
return start_value + (end_value >= start_value ? 1.f : -1.f) * band * step;
|
||||
}
|
||||
return start_value + ratio * (end_value - start_value);
|
||||
}
|
||||
|
||||
std::string encodeBase64(uint64_t value)
|
||||
@@ -8655,7 +8956,7 @@ void GCode::update_placeholder_parser_with_variant_params()
|
||||
}
|
||||
}
|
||||
|
||||
std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override)
|
||||
std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override, bool defer_temp_wait)
|
||||
{
|
||||
int new_extruder_id = get_extruder_id(new_filament_id);
|
||||
if (!m_writer.need_toolchange(new_filament_id))
|
||||
@@ -8754,7 +9055,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
// per-layer nozzle grouping; resolve the column instead of indexing by the filament id.
|
||||
size_t new_fi = get_filament_config_index((int)new_filament_id);
|
||||
float new_retract_length = m_config.retraction_length.get_at(new_fi);
|
||||
float new_retract_length_toolchange = m_config.retract_length_toolchange.get_at(new_filament_id);
|
||||
float new_retract_length_toolchange = m_config.retract_length_toolchange.get_at(new_fi);
|
||||
int new_filament_temp = this->on_first_layer() ? m_config.nozzle_temperature_initial_layer.get_at(new_fi) : m_config.nozzle_temperature.get_at(new_fi);
|
||||
// BBS: if print_z == 0 use first layer temperature
|
||||
if (abs(print_z) < EPSILON)
|
||||
@@ -8762,6 +9063,24 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
if (toolchange_temp_override > 0)
|
||||
new_filament_temp = toolchange_temp_override;
|
||||
|
||||
// With wait_for_temp_on_wipe_tower the blocking M109 is deferred to the wipe tower, so raise
|
||||
// the incoming filament's target here — ahead of the tool change rather than after it — and
|
||||
// let the heat-up overlap the change itself as well as the travel to the tower. The command
|
||||
// always carries an explicit tool index (the option is off for single extruder MM, so the
|
||||
// writer emits one), leaving the outgoing filament that pre_toolchange just dropped to its
|
||||
// standby temperature alone. nozzle_temperature == 0 means "use the first layer temperature".
|
||||
if (defer_temp_wait) {
|
||||
// Target what the tower will wait on. It waits on the first layer temperature not only on
|
||||
// the first layer but also while priming, which runs before any layer is set: there
|
||||
// on_first_layer() is false and print_z is the initial layer height, so neither test above
|
||||
// catches it. nozzle_temperature == 0 means "use the first layer temperature" as well.
|
||||
int preheat_temp = new_filament_temp;
|
||||
if (toolchange_temp_override <= 0 && (m_layer == nullptr || preheat_temp <= 0))
|
||||
preheat_temp = m_config.nozzle_temperature_initial_layer.get_at(new_fi);
|
||||
if (preheat_temp > 0)
|
||||
gcode += m_writer.set_temperature(preheat_temp, false, new_filament_id);
|
||||
}
|
||||
|
||||
Vec3d nozzle_pos = m_writer.get_position();
|
||||
float old_retract_length, old_retract_length_toolchange, wipe_volume;
|
||||
int old_filament_temp, old_filament_e_feedrate;
|
||||
@@ -8785,7 +9104,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
// gap-filled carry-forward, so its current-layer column matches the nozzle it occupies.
|
||||
size_t old_fi = get_filament_config_index(old_filament_id);
|
||||
old_retract_length = m_config.retraction_length.get_at(old_fi);
|
||||
old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_filament_id);
|
||||
old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_fi);
|
||||
old_filament_temp = this->on_first_layer()? m_config.nozzle_temperature_initial_layer.get_at(old_fi) : m_config.nozzle_temperature.get_at(old_fi);
|
||||
|
||||
//During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length.
|
||||
@@ -9065,8 +9384,10 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
|
||||
}
|
||||
check_add_eol(gcode);
|
||||
}
|
||||
// Set the new extruder to the operating temperature.
|
||||
if (m_ooze_prevention.enable)
|
||||
// Set the new extruder to the operating temperature. With defer_temp_wait the target was
|
||||
// already raised before the tool change and the blocking wait belongs to the wipe tower
|
||||
// generator, so there is nothing left to restore here.
|
||||
if (m_ooze_prevention.enable && !defer_temp_wait)
|
||||
gcode += m_ooze_prevention.post_toolchange(*this);
|
||||
|
||||
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {
|
||||
|
||||
+40
-3
@@ -130,8 +130,11 @@ public:
|
||||
private:
|
||||
WipeTowerIntegration& operator=(const WipeTowerIntegration&);
|
||||
std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
|
||||
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const BoundingBox &printer_bbx) const;
|
||||
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const;
|
||||
std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
|
||||
std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const;
|
||||
Vec2f transform_wt2_pt(const Vec2f &pt) const;
|
||||
Polygons shared_printable_area(GCode &gcodegen) const;
|
||||
|
||||
// Postprocesses gcode: rotates and moves G1 extrusions and returns result
|
||||
std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const;
|
||||
@@ -181,7 +184,7 @@ struct LayerResult {
|
||||
// It is used for the pressure equalizer because it needs to buffer one layer back.
|
||||
bool nop_layer_result { false };
|
||||
|
||||
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<coord_t>::max(), false, false, true}; }
|
||||
static LayerResult make_nop_layer_result() { return {"", std::numeric_limits<size_t>::max(), false, false, true}; }
|
||||
};
|
||||
|
||||
class GCode {
|
||||
@@ -259,7 +262,7 @@ public:
|
||||
std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone);
|
||||
// extra_retract forwards a PETG pre-extrusion over-extrusion; default 0 -> identical to the plain deretract.
|
||||
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
|
||||
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1);
|
||||
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false);
|
||||
bool is_BBL_Printer();
|
||||
WipeTowerType wipe_tower_type();
|
||||
|
||||
@@ -539,6 +542,40 @@ private:
|
||||
// Cache for custom seam enforcers/blockers for each layer.
|
||||
SeamPlacer m_seam_placer;
|
||||
|
||||
// One stop of the island-level tour: consecutive islands of a single instance. An instance
|
||||
// can have several visits per layer when its islands are toured non-consecutively.
|
||||
struct InstanceVisit
|
||||
{
|
||||
// Index into the per-filament InstanceToPrint vector.
|
||||
size_t instance_idx;
|
||||
// Islands to print, in order (indices into ObjectByExtruder::islands). Empty: print all
|
||||
// islands, ordered at extrusion time.
|
||||
std::vector<size_t> islands;
|
||||
// First visit of this instance this layer; skirt, brim and support are emitted here.
|
||||
bool first_visit;
|
||||
};
|
||||
|
||||
// One node of the island-level tour, also used as cache key: identity plus quantized position.
|
||||
struct IslandOrderNode
|
||||
{
|
||||
ObjectID object_id;
|
||||
size_t instance_id;
|
||||
// Index into ObjectByExtruder::islands, or size_t(-1) for an instance without chainable
|
||||
// islands (e.g. support only), which is toured as a single stop.
|
||||
size_t island_idx;
|
||||
// Island centroid in G-code coordinates, quantized to 1 mm for cache stability.
|
||||
Point pos;
|
||||
bool operator==(const IslandOrderNode &rhs) const {
|
||||
return object_id == rhs.object_id && instance_id == rhs.instance_id &&
|
||||
island_idx == rhs.island_idx && pos == rhs.pos;
|
||||
}
|
||||
};
|
||||
|
||||
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
|
||||
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
|
||||
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
|
||||
m_ordering_cache;
|
||||
|
||||
ExtrusionQualityEstimator m_extrusion_quality_estimator;
|
||||
|
||||
|
||||
|
||||
@@ -844,7 +844,10 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
ironing_fan_control = false; // ORCA: Add support for ironing fan speed control
|
||||
ironing_fan_speed = 0; // ORCA: Add support for ironing fan speed control
|
||||
}
|
||||
if (fan_speed_new != m_fan_speed) {
|
||||
// A tool change may keep the same configured base fan speed while the physical fan is
|
||||
// still running at the previous filament's overhang speed. Restore the base speed before
|
||||
// emitting G-code for the new tool in that case.
|
||||
if (fan_speed_new != m_fan_speed || (immediately_apply && m_current_fan_speed != fan_speed_new)) {
|
||||
m_fan_speed = fan_speed_new;
|
||||
m_current_fan_speed = fan_speed_new;
|
||||
if (immediately_apply)
|
||||
@@ -1040,8 +1043,10 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm);
|
||||
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
|
||||
}
|
||||
else
|
||||
else {
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm);
|
||||
m_current_fan_speed = m_fan_speed;
|
||||
}
|
||||
need_set_fan = false;
|
||||
}
|
||||
pos = line_end;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <unordered_map>
|
||||
@@ -39,7 +40,11 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
|
||||
const AABBTreeLines::LinesDistancer<L>& unscaled_prev_layer,
|
||||
float flow_width,
|
||||
float max_line_length = -1.0f,
|
||||
float min_distance = -1.0f)
|
||||
float min_distance = -1.0f,
|
||||
// Maps an overhang distance onto the speed it will be printed at. Interior sampling
|
||||
// needs it to tell which of the points it could add would change the G-code, and is
|
||||
// skipped without it.
|
||||
const std::function<float(float)>& distance_to_speed = {})
|
||||
{
|
||||
bool looped = input_points.front() == input_points.back();
|
||||
std::function<size_t(size_t,size_t)> get_prev_index = [](size_t idx, size_t count) {
|
||||
@@ -120,6 +125,107 @@ std::vector<ExtendedPoint<L::Dim>> estimate_points_properties(const POINTS&
|
||||
points.push_back(next_point);
|
||||
}
|
||||
|
||||
// ORCA: Interior sampling
|
||||
// The passes below infer the support under a span from its endpoints alone, so an interior that is supported
|
||||
// differently from both ends is invisible to them: the outer perimeter of an overhang whose ends are caged by
|
||||
// full height walls reads as supported along its whole length. Probe the interior, keep the samples the
|
||||
// endpoint interpolation fails to predict, and bisect either side of each one, so a span that is only partly
|
||||
// unsupported gets points where its support actually changes instead of one reading spread across all of it.
|
||||
if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS && min_distance > 0 && distance_to_speed) {
|
||||
// Probe at least this densely before treating matching samples as evidence that a span is uniform. The
|
||||
// segmentation pass below only splits lines of 2mm or more, and every pass here drops points closer
|
||||
// together than min_spacing, so finer discovery would not produce a more precise speed transition.
|
||||
const double max_probe_spacing = std::max(2., 4. * min_spacing);
|
||||
// A backstop for that length test, which on a non-finite length would never be met.
|
||||
constexpr int max_bisection_depth = 10;
|
||||
// Whether two readings are interchangeable. A segment is printed at the lower of the speeds its ends
|
||||
// read, so a sample that agrees on speed with what is already known cannot change the G-code, whatever
|
||||
// its distance says. The distances themselves are far too coarse a stand-in for this: the speed sections
|
||||
// interpolate, so readings a small fraction of min_distance apart can still be tens of mm/s apart.
|
||||
// The tolerance matches the one GCode.cpp applies when it decides a path has a variable speed at all.
|
||||
auto same_speed = [&distance_to_speed](float a, float b) {
|
||||
return std::abs(distance_to_speed(a) - distance_to_speed(b)) <= 1.f;
|
||||
};
|
||||
// Whether the first reading is printed slower than the second, once they are known to differ.
|
||||
auto prints_slower = [&distance_to_speed](float a, float b) { return distance_to_speed(a) < distance_to_speed(b); };
|
||||
|
||||
// Part of a segment still to bisect: its positions along the segment and bisections left.
|
||||
struct Subspan { double t0, t1; int depth; };
|
||||
|
||||
std::vector<ExtendedPoint<L::Dim>> sampled_points; // Populated lazily, on the first insertion
|
||||
std::vector<std::pair<double, float>> interior; // Samples of one segment, keyed by position along it
|
||||
std::vector<Subspan> pending;
|
||||
|
||||
for (size_t point_idx = 0; point_idx + 1 < points.size(); ++point_idx) {
|
||||
const ExtendedPoint<L::Dim>& curr = points[point_idx];
|
||||
const ExtendedPoint<L::Dim>& next = points[point_idx + 1];
|
||||
const Vec step = next.position - curr.position;
|
||||
const double line_len = step.norm();
|
||||
|
||||
interior.clear();
|
||||
if (line_len >= max_probe_spacing)
|
||||
pending.push_back({0., 1., max_bisection_depth});
|
||||
|
||||
while (!pending.empty()) {
|
||||
const Subspan subspan = pending.back();
|
||||
pending.pop_back();
|
||||
if (subspan.depth <= 0 || (subspan.t1 - subspan.t0) * line_len < max_probe_spacing)
|
||||
continue;
|
||||
|
||||
const double t = 0.5 * (subspan.t0 + subspan.t1);
|
||||
auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra<SIGNED_DISTANCE>(
|
||||
(curr.position + t * step).template cast<AABBScalar>());
|
||||
const float sampled = float(distance + boundary_offset);
|
||||
|
||||
interior.emplace_back(t, sampled);
|
||||
pending.push_back({subspan.t0, t, subspan.depth - 1});
|
||||
pending.push_back({t, subspan.t1, subspan.depth - 1});
|
||||
}
|
||||
|
||||
if (!interior.empty()) {
|
||||
std::sort(interior.begin(), interior.end(),
|
||||
[](const std::pair<double, float>& l, const std::pair<double, float>& r) { return l.first < r.first; });
|
||||
// Coarse probing keeps every sample it took until this pass can see which ones bracket a speed
|
||||
// transition. Matching samples cannot be discarded during discovery: one may be the last
|
||||
// supported point before a narrow unsupported pocket found by a later probe.
|
||||
size_t kept = 0;
|
||||
for (size_t i = 0; i < interior.size(); ++i) {
|
||||
const float sample = interior[i].second;
|
||||
const bool at_start = kept == 0; // Nothing kept yet, so the segment's own start precedes it
|
||||
const bool at_end = i + 1 == interior.size(); // And nothing follows the last sample but the segment's end
|
||||
const float before = at_start ? curr.distance : interior[kept - 1].second;
|
||||
const float after = at_end ? next.distance : interior[i + 1].second;
|
||||
// A sample is worth a point in the path only where it prints at a different speed from the
|
||||
// readings either side of it. Differing from one of the segment's own ends is not enough on
|
||||
// its own where the sample is the faster of the two: the segmentation pass below already
|
||||
// ends the slowdown an end reads, at a distance taken from how far out that end is rather
|
||||
// than from wherever bisection happened to stop, and a point here would leave the span
|
||||
// beside the end too short for that pass to run at all. Support an end cannot account for,
|
||||
// where the interior is the slower reading, is exactly what this pass is here to find.
|
||||
const bool worth_before = !same_speed(sample, before) && (!at_start || prints_slower(sample, before));
|
||||
const bool worth_after = !same_speed(sample, after) && (!at_end || prints_slower(sample, after));
|
||||
if (worth_before || worth_after)
|
||||
interior[kept++] = interior[i];
|
||||
}
|
||||
interior.resize(kept);
|
||||
}
|
||||
|
||||
if (!interior.empty() && sampled_points.empty()) {
|
||||
sampled_points.reserve(points.size() + 8);
|
||||
sampled_points.assign(points.begin(), points.begin() + point_idx + 1);
|
||||
}
|
||||
if (!sampled_points.empty()) {
|
||||
// Only a sub-span of max_probe_spacing or more is ever bisected, so these sit at least
|
||||
// 2 * min_spacing apart, and need none of the filtering the passes either side of this one do.
|
||||
for (const auto& [t, distance] : interior)
|
||||
sampled_points.push_back({curr.position + t * step, distance});
|
||||
sampled_points.push_back(next);
|
||||
}
|
||||
}
|
||||
if (!sampled_points.empty())
|
||||
points = std::move(sampled_points);
|
||||
}
|
||||
|
||||
// Segmentation handling
|
||||
if (PREV_LAYER_BOUNDARY_OFFSET && ADD_INTERSECTIONS) {
|
||||
std::vector<ExtendedPoint<L::Dim>> new_points;
|
||||
@@ -362,9 +468,28 @@ public:
|
||||
smallest_distance_with_lower_speed=-1.f;
|
||||
|
||||
// Orca: Pass to the point properties estimator the smallest ovehang distance that triggers a slowdown (smallest_distance_with_lower_speed)
|
||||
auto calculate_speed = [&speed_sections, &original_speed](float distance) {
|
||||
float final_speed;
|
||||
if (distance <= speed_sections.front().first) {
|
||||
final_speed = original_speed;
|
||||
} else if (distance >= speed_sections.back().first) {
|
||||
final_speed = speed_sections.back().second;
|
||||
} else {
|
||||
size_t section_idx = 0;
|
||||
while (distance > speed_sections[section_idx + 1].first) {
|
||||
section_idx++;
|
||||
}
|
||||
float t = (distance - speed_sections[section_idx].first) /
|
||||
(speed_sections[section_idx + 1].first - speed_sections[section_idx].first);
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second;
|
||||
}
|
||||
return round(final_speed);
|
||||
};
|
||||
|
||||
std::vector<ExtendedPoint<3>> extended_points =
|
||||
estimate_points_properties<true, true, true, true>(path.polyline.points, prev_layer_boundaries[current_object], path.width, -1,
|
||||
smallest_distance_with_lower_speed);
|
||||
smallest_distance_with_lower_speed, calculate_speed);
|
||||
const auto width_inv = 1.0f / path.width;
|
||||
std::vector<ProcessedPoint> processed_points;
|
||||
processed_points.reserve(extended_points.size());
|
||||
@@ -423,25 +548,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
auto calculate_speed = [&speed_sections, &original_speed](float distance) {
|
||||
float final_speed;
|
||||
if (distance <= speed_sections.front().first) {
|
||||
final_speed = original_speed;
|
||||
} else if (distance >= speed_sections.back().first) {
|
||||
final_speed = speed_sections.back().second;
|
||||
} else {
|
||||
size_t section_idx = 0;
|
||||
while (distance > speed_sections[section_idx + 1].first) {
|
||||
section_idx++;
|
||||
}
|
||||
float t = (distance - speed_sections[section_idx].first) /
|
||||
(speed_sections[section_idx + 1].first - speed_sections[section_idx].first);
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
final_speed = (1.0f - t) * speed_sections[section_idx].second + t * speed_sections[section_idx + 1].second;
|
||||
}
|
||||
return round(final_speed);
|
||||
};
|
||||
|
||||
float extrusion_speed = std::min(calculate_speed(curr.distance), calculate_speed(next.distance));
|
||||
// ORCA: Clamp resulting speed to lowest of calculated speed based on the overhang values and the current speed
|
||||
// Fixes bug where resulting overhang speed is higher than the current speed due to (for example) volumetric flow limits.
|
||||
|
||||
@@ -614,6 +614,8 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
float leftover = 0.0f;
|
||||
for (size_t i = additional_buffer_idx; i < additional_buffer.size(); ++i)
|
||||
leftover += additional_buffer[i].second;
|
||||
BOOST_LOG_TRIVIAL(debug) << "calculate_time(is_final): leftover=" << leftover
|
||||
<< "s from " << (additional_buffer.size() - additional_buffer_idx) << " items";
|
||||
time += double(leftover);
|
||||
gcode_time.cache += leftover;
|
||||
} else {
|
||||
@@ -1448,8 +1450,8 @@ void GCodeProcessor::run_post_process()
|
||||
// flag) runs none of this. It is pure data construction — it only fills m_filament_blocks /
|
||||
// m_extruder_blocks / m_machine_*_gcode_*_line_id and never touches the exported g-code, so even
|
||||
// the enable_pre_heating fleet stays byte-identical (nothing reads the blocks until the injection
|
||||
// pass). In practice it also stays empty/degenerate today because no template/code yet emits the
|
||||
// MACHINE_*_GCODE_* / NOZZLE_CHANGE_* / CP_TOOLCHANGE_WIPE markers it keys off.
|
||||
// pass). The wipe tower emits the NOZZLE_CHANGE_* (ramming) and CP_TOOLCHANGE_WIPE markers this
|
||||
// builder keys off; the MACHINE_*_GCODE_* markers come from the machine g-code templates.
|
||||
m_filament_blocks.clear();
|
||||
m_extruder_blocks.clear();
|
||||
m_machine_start_gcode_end_line_id = (unsigned int) (-1);
|
||||
@@ -3492,6 +3494,7 @@ void GCodeProcessor::reset()
|
||||
m_extruder_blocks.clear();
|
||||
m_machine_start_gcode_end_line_id = (unsigned int) (-1);
|
||||
m_machine_end_gcode_start_line_id = (unsigned int) (-1);
|
||||
m_skip_end_gcode_delays = false;
|
||||
m_remaining_volume = std::vector<float>(MAXIMUM_EXTRUDER_NUMBER, 0.f);
|
||||
|
||||
m_line_id = 0;
|
||||
@@ -4219,6 +4222,14 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
|
||||
return;
|
||||
}
|
||||
|
||||
// End gcode marker: skip post-print M400 S/P dwells after this point so the M73 estimate reports
|
||||
// print-completion time, not post-print filtration/cooldown. BBS drops the same remainder in
|
||||
// calculate_time(is_final).
|
||||
if (comment == Machine_End_GCode_Start_Tag) {
|
||||
m_skip_end_gcode_delays = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Orca: Integrate filament consumption for purging performed to an external device and controlled via macros
|
||||
// (eg. Happy Hare) in the filament consumption stats.
|
||||
if (boost::starts_with(comment, GCodeProcessor::External_Purge_Tag)) {
|
||||
@@ -5915,8 +5926,11 @@ void GCodeProcessor::process_G10(const GCodeReader::GCodeLine& line)
|
||||
GCodeReader::GCodeLine g10;
|
||||
g10.set(Axis::E, -this->m_parser.config().retraction_length.get_at(m_extruder_id));
|
||||
g10.set(Axis::F, this->m_parser.config().retraction_speed.get_at(m_extruder_id) * 60);
|
||||
//Orca: Firmware retract emulation must not change the modal G1 feedrate.
|
||||
const float feedrate = m_feedrate;
|
||||
--m_g1_line_id;
|
||||
process_G1(g10);
|
||||
m_feedrate = feedrate;
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line)
|
||||
@@ -5925,8 +5939,11 @@ void GCodeProcessor::process_G11(const GCodeReader::GCodeLine& line)
|
||||
GCodeReader::GCodeLine g11;
|
||||
g11.set(Axis::E, this->m_parser.config().retraction_length.get_at(m_extruder_id) + this->m_parser.config().retract_restart_extra.get_at(m_extruder_id));
|
||||
g11.set(Axis::F, this->m_parser.config().deretraction_speed.get_at(m_extruder_id) * 60);
|
||||
// Orca: Firmware unretract emulation must not change the modal G1 feedrate.
|
||||
const float feedrate = m_feedrate;
|
||||
--m_g1_line_id;
|
||||
process_G1(g11);
|
||||
m_feedrate = feedrate;
|
||||
}
|
||||
|
||||
void GCodeProcessor::process_G20(const GCodeReader::GCodeLine& line)
|
||||
@@ -6401,6 +6418,10 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
|
||||
float value_p = 0.0;
|
||||
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
|
||||
value_s += value_p * 0.001;
|
||||
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
||||
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
||||
if (m_skip_end_gcode_delays)
|
||||
return;
|
||||
simulate_st_synchronize(value_s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1109,6 +1109,10 @@ class Print;
|
||||
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
|
||||
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
|
||||
unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) };
|
||||
// Set when the MACHINE_END_GCODE_START tag is seen during the streaming parse; tells
|
||||
// process_M400 to skip post-print end-gcode dwells (air purification, timelapse, sound)
|
||||
// so they don't inflate the M73 estimate. BBS excludes them in calculate_time(is_final).
|
||||
bool m_skip_end_gcode_delays{ false };
|
||||
// Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each
|
||||
// extruder currently carries. Written by both branches of the two-arg process_filament_change
|
||||
// (the fallback branch does occupancy bookkeeping only); read by the richer change-time model
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
// Print-object ordering strategies: implementation.
|
||||
// Consolidates TSP post-processing, Snake, and Best-of-Strategies.
|
||||
|
||||
#include "OrderingStrategies.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
#include "../ShortestPath.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
/* ====================================================================
|
||||
* TSP post-processing utilities
|
||||
* ==================================================================== */
|
||||
|
||||
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes)
|
||||
{
|
||||
size_t pn = path.size();
|
||||
if (pn <= 2) return false;
|
||||
|
||||
// Pre-compute edge lengths once per pass to avoid redundant norm() calls.
|
||||
auto recompute_edges = [&]() {
|
||||
std::vector<double> el(pn);
|
||||
for (size_t i = 0; i < pn; ++i) {
|
||||
size_t ni = (i + 1) % pn;
|
||||
el[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).norm();
|
||||
}
|
||||
return el;
|
||||
};
|
||||
std::vector<double> el = recompute_edges();
|
||||
|
||||
// Pre-compute squared edge lengths for early rejection in the inner loop.
|
||||
auto recompute_edges_sq = [&]() {
|
||||
std::vector<double> elsq(pn);
|
||||
for (size_t i = 0; i < pn; ++i) {
|
||||
size_t ni = (i + 1) % pn;
|
||||
elsq[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).squaredNorm();
|
||||
}
|
||||
return elsq;
|
||||
};
|
||||
std::vector<double> elsq = recompute_edges_sq();
|
||||
|
||||
bool improved = false;
|
||||
for (int pass = 0; max_passes <= 0 || pass < max_passes; ++pass) {
|
||||
size_t best_i = pn, best_j = pn;
|
||||
double best_gain = 0;
|
||||
|
||||
for (size_t i = 0; i < pn; ++i) {
|
||||
const Vec2d& pi = centers[path[i]].cast<double>();
|
||||
const Vec2d& p_in = centers[path[(i + 1) % pn]].cast<double>();
|
||||
double d_i = el[i];
|
||||
double d_i_sq = elsq[i];
|
||||
|
||||
for (size_t j = i + 2; j < pn; ++j) {
|
||||
size_t j_next = (j + 1) % pn;
|
||||
// Skip the swap that would reverse the entire cycle (removes both
|
||||
// edges (0,1) and (pn-1,0), equivalent to traversing the cycle backwards).
|
||||
if (i == 0 && j_next == 0) continue;
|
||||
|
||||
const Vec2d& pj = centers[path[j]].cast<double>();
|
||||
const Vec2d& p_jn = centers[path[j_next]].cast<double>();
|
||||
double d_j = el[j];
|
||||
|
||||
// Early rejection using squared distances (avoids 2 sqrt calls).
|
||||
double new_a_sq = (pj - pi).squaredNorm();
|
||||
double new_b_sq = (p_jn - p_in).squaredNorm();
|
||||
if (new_a_sq >= d_i_sq && new_b_sq >= elsq[j]) continue;
|
||||
|
||||
double new_a = std::sqrt(new_a_sq);
|
||||
double new_b = std::sqrt(new_b_sq);
|
||||
double gain = d_i + d_j - new_a - new_b;
|
||||
|
||||
if (gain > best_gain) {
|
||||
best_gain = gain;
|
||||
best_i = i; best_j = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (best_i == pn) break;
|
||||
improved = true;
|
||||
// Reverse the best swap segment
|
||||
std::reverse(path.begin() + best_i + 1, path.begin() + best_j + 1);
|
||||
|
||||
// Recompute edge lengths after reversal
|
||||
el = recompute_edges();
|
||||
elsq = recompute_edges_sq();
|
||||
}
|
||||
return improved;
|
||||
}
|
||||
|
||||
// Fast bounding-box overlap test (rejects most non-intersecting pairs).
|
||||
static inline bool bboxes_overlap(const Point& a, const Point& b, const Point& c, const Point& d)
|
||||
{
|
||||
return !(std::max(a.x(), b.x()) < std::min(c.x(), d.x()) ||
|
||||
std::max(c.x(), d.x()) < std::min(a.x(), b.x()) ||
|
||||
std::max(a.y(), b.y()) < std::min(c.y(), d.y()) ||
|
||||
std::max(c.y(), d.y()) < std::min(a.y(), b.y()));
|
||||
}
|
||||
|
||||
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
size_t pn = path.size();
|
||||
if (pn <= 3) return false;
|
||||
|
||||
// Treat path as a cycle: include the closing edge (pn-1 -> 0), consistent with the other
|
||||
// TSP helpers (2-opt, closing-edge rotation) that operate on the full cycle.
|
||||
size_t n_edges = pn;
|
||||
|
||||
// Scan for first crossing; returns {i, j} or {npos, npos} if none.
|
||||
auto find_crossing = [&]() -> std::pair<size_t, size_t> {
|
||||
for (size_t i = 0; i < n_edges; ++i) {
|
||||
const Point& ai = centers[path[i]];
|
||||
const Point& bi = centers[path[(i + 1) % pn]];
|
||||
|
||||
for (size_t j = i + 2; j < n_edges; ++j) {
|
||||
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
|
||||
if (i == 0 && j == pn - 1) continue;
|
||||
|
||||
const Point& aj = centers[path[j]];
|
||||
const Point& bj = centers[path[(j + 1) % pn]];
|
||||
|
||||
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
|
||||
if (Geometry::segments_intersect(ai, bi, aj, bj))
|
||||
return {i, j};
|
||||
}
|
||||
}
|
||||
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
|
||||
};
|
||||
|
||||
// Process crossings one at a time: find first, reverse it, restart scan.
|
||||
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
|
||||
int max_iters = static_cast<int>(pn * pn);
|
||||
bool improved = false;
|
||||
while (max_iters-- > 0) {
|
||||
auto [ci, cj] = find_crossing();
|
||||
if (ci == std::numeric_limits<size_t>::max()) break;
|
||||
improved = true;
|
||||
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
|
||||
}
|
||||
return improved;
|
||||
}
|
||||
|
||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
size_t pn = path.size();
|
||||
size_t best_start = 0;
|
||||
double best_closing2 = std::numeric_limits<double>::max();
|
||||
for (size_t start = 0; start < pn; ++start) {
|
||||
size_t last = (start + pn - 1) % pn;
|
||||
double d2 = (centers[path[start]].cast<double>() - centers[path[last]].cast<double>()).squaredNorm();
|
||||
if (d2 < best_closing2) { best_closing2 = d2; best_start = start; }
|
||||
}
|
||||
std::rotate(path.begin(), path.begin() + best_start, path.end());
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Snake ordering
|
||||
* ==================================================================== */
|
||||
|
||||
struct SnakeRow { double avg_y; std::vector<size_t> indices; };
|
||||
|
||||
// --- Row threshold computation ---
|
||||
// Extract unique Y values and use the median gap between them to determine
|
||||
// the row threshold.
|
||||
static double compute_row_threshold(const std::vector<double>& sorted_ys,
|
||||
double y_min, double y_max,
|
||||
size_t n,
|
||||
double fraction_of_y_range,
|
||||
double min_threshold_um)
|
||||
{
|
||||
constexpr double MIN_GAP_FILTER = 1.0; // ignore sub-micron gaps (coord_t = 1/100mm)
|
||||
|
||||
// Extract unique Y values
|
||||
std::vector<double> unique_ys;
|
||||
unique_ys.reserve(sorted_ys.size());
|
||||
unique_ys.push_back(sorted_ys[0]);
|
||||
for (size_t i = 1; i < sorted_ys.size(); ++i) {
|
||||
if (sorted_ys[i] - sorted_ys[i - 1] > MIN_GAP_FILTER)
|
||||
unique_ys.push_back(sorted_ys[i]);
|
||||
}
|
||||
|
||||
double fallback_threshold = (y_max - y_min) * fraction_of_y_range;
|
||||
if (unique_ys.size() <= 1) {
|
||||
return std::max(fallback_threshold, min_threshold_um);
|
||||
}
|
||||
|
||||
// Compute gaps between consecutive unique Y values
|
||||
std::vector<double> gaps;
|
||||
gaps.reserve(unique_ys.size() - 1);
|
||||
for (size_t i = 1; i < unique_ys.size(); ++i)
|
||||
gaps.push_back(unique_ys[i] - unique_ys[i - 1]);
|
||||
|
||||
if (gaps.empty()) {
|
||||
return std::max(fallback_threshold, min_threshold_um);
|
||||
}
|
||||
|
||||
// Sort gaps to find the median
|
||||
std::sort(gaps.begin(), gaps.end());
|
||||
double median_gap = gaps[gaps.size() / 2];
|
||||
double min_gap = gaps.front();
|
||||
|
||||
// Threshold: half the gap between consecutive unique Y values.
|
||||
double threshold = (median_gap < min_gap * 1.5) ? min_gap * 0.5 : median_gap * 0.5;
|
||||
|
||||
bool has_row_structure;
|
||||
if (unique_ys.size() * 2 <= n) {
|
||||
has_row_structure = true;
|
||||
} else {
|
||||
// Single-column or sparse: uniform gaps indicate a deliberate grid
|
||||
double max_gap = *std::max_element(gaps.begin(), gaps.end());
|
||||
has_row_structure = (max_gap < min_gap * 2.0);
|
||||
}
|
||||
|
||||
if (has_row_structure) {
|
||||
// For grid-like data, use the gap-based threshold directly.
|
||||
return threshold;
|
||||
}
|
||||
|
||||
return std::max(fallback_threshold, min_threshold_um);
|
||||
}
|
||||
|
||||
// --- Row grouping ---
|
||||
// Bin points into rows by quantising Y / threshold
|
||||
static std::vector<SnakeRow> group_into_rows(const Points& centers, double row_threshold)
|
||||
{
|
||||
size_t n = centers.size();
|
||||
std::unordered_map<int64_t, std::vector<size_t>> row_map;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
int64_t y_key = static_cast<int64_t>(std::floor(static_cast<double>(centers[i].y()) / row_threshold));
|
||||
row_map[y_key].push_back(i);
|
||||
}
|
||||
|
||||
std::vector<SnakeRow> rows;
|
||||
rows.reserve(row_map.size());
|
||||
for (auto& [key, indices] : row_map) {
|
||||
double avg_y = std::accumulate(indices.begin(), indices.end(), 0.0,
|
||||
[&](double acc, size_t idx) { return acc + static_cast<double>(centers[idx].y()); })
|
||||
/ indices.size();
|
||||
rows.push_back({avg_y, std::move(indices)});
|
||||
}
|
||||
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const SnakeRow& a, const SnakeRow& b) { return a.avg_y < b.avg_y; });
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
// Sort each row by X and greedily pick the direction (left->right or right->left)
|
||||
// that minimises the transition distance from the previous row's endpoint.
|
||||
static std::vector<size_t> build_serpentine_path(const Points& centers,
|
||||
std::vector<SnakeRow>& rows)
|
||||
{
|
||||
std::vector<size_t> path;
|
||||
path.reserve(centers.size());
|
||||
|
||||
for (size_t ri = 0; ri < rows.size(); ++ri) {
|
||||
auto& row = rows[ri].indices;
|
||||
std::sort(row.begin(), row.end(),
|
||||
[&](size_t a, size_t b) { return centers[a].x() < centers[b].x(); });
|
||||
|
||||
if (ri == 0) {
|
||||
path.insert(path.end(), row.begin(), row.end());
|
||||
} else {
|
||||
const Point& prev_end = centers[path.back()];
|
||||
double dist_to_left = (prev_end.cast<double>() - centers[row.front()].cast<double>()).squaredNorm();
|
||||
double dist_to_right = (prev_end.cast<double>() - centers[row.back()].cast<double>()).squaredNorm();
|
||||
|
||||
if (dist_to_left <= dist_to_right)
|
||||
path.insert(path.end(), row.begin(), row.end());
|
||||
else
|
||||
path.insert(path.end(), row.rbegin(), row.rend());
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// Row-based serpentine traversal: detect rows, bin points, snake through them.
|
||||
static std::vector<size_t> row_serpentine_path(const Points& centers,
|
||||
double fraction_of_y_range = 0.02,
|
||||
double min_threshold_um = 1e4)
|
||||
{
|
||||
if (centers.empty()) return {};
|
||||
|
||||
size_t n = centers.size();
|
||||
|
||||
// Collect and sort Y coordinates.
|
||||
std::vector<double> sorted_ys;
|
||||
sorted_ys.reserve(n);
|
||||
for (const auto& p : centers) sorted_ys.push_back(static_cast<double>(p.y()));
|
||||
std::sort(sorted_ys.begin(), sorted_ys.end());
|
||||
|
||||
auto [ymin, ymax] = std::minmax_element(sorted_ys.begin(), sorted_ys.end());
|
||||
double y_min = *ymin, y_max = *ymax;
|
||||
|
||||
double row_threshold = compute_row_threshold(sorted_ys, y_min, y_max, n,
|
||||
fraction_of_y_range, min_threshold_um);
|
||||
|
||||
auto rows = group_into_rows(centers, row_threshold);
|
||||
return build_serpentine_path(centers, rows);
|
||||
}
|
||||
|
||||
std::vector<size_t> snake_core(const Points& centers)
|
||||
{
|
||||
if (centers.empty()) return {};
|
||||
|
||||
std::vector<size_t> path = row_serpentine_path(centers);
|
||||
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
bool improved = tsp_2opt_improve(path, centers);
|
||||
improved |= tsp_remove_crossings(path, centers);
|
||||
if (!improved) break;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
|
||||
{
|
||||
return chain_instances_with_core(print_objects, start_near, snake_core);
|
||||
}
|
||||
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print)
|
||||
{
|
||||
return chain_print_object_instances_snake(print.objects().vector(), nullptr);
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Best-of-strategies meta-strategy
|
||||
* ==================================================================== */
|
||||
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
|
||||
{
|
||||
if (print_objects.empty())
|
||||
return {};
|
||||
|
||||
// Run all strategies.
|
||||
std::vector<std::vector<const PrintInstance*>> candidates;
|
||||
candidates.push_back(chain_print_object_instances(print_objects, start_near));
|
||||
candidates.push_back(chain_print_object_instances_snake(print_objects, start_near));
|
||||
|
||||
// Compute metrics for each candidate.
|
||||
struct Candidate { double total_len; double max_edge; };
|
||||
std::vector<Candidate> metrics;
|
||||
metrics.reserve(candidates.size());
|
||||
|
||||
for (size_t i = 0; i < candidates.size(); ++i) {
|
||||
double total = 0.0;
|
||||
double mx = 0.0;
|
||||
for (size_t j = 0; j < candidates[i].size(); ++j) {
|
||||
size_t k = (j + 1) % candidates[i].size();
|
||||
double d = (candidates[i][j]->shift.cast<double>() - candidates[i][k]->shift.cast<double>()).norm();
|
||||
total += d;
|
||||
if (d > mx) mx = d;
|
||||
}
|
||||
metrics.push_back({total, mx});
|
||||
}
|
||||
|
||||
// Pick shortest total path; tiebreak on smallest max edge.
|
||||
auto best_it = std::min_element(metrics.begin(), metrics.end(),
|
||||
[](const Candidate& a, const Candidate& b) {
|
||||
return a.total_len < b.total_len ||
|
||||
(a.total_len == b.total_len && a.max_edge < b.max_edge);
|
||||
});
|
||||
size_t best = static_cast<size_t>(std::distance(metrics.begin(), best_it));
|
||||
|
||||
return candidates[best];
|
||||
}
|
||||
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print)
|
||||
{
|
||||
return chain_print_object_instances_best_of(print.objects().vector(), nullptr);
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Island-level ordering entry point
|
||||
* ==================================================================== */
|
||||
|
||||
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near)
|
||||
{
|
||||
if (points.empty())
|
||||
return {};
|
||||
|
||||
if (print_order != PrintOrder::Snake && print_order != PrintOrder::BestOfStrategies)
|
||||
// Nearest neighbor + post-processing; honours start_near natively.
|
||||
return chain_points_with_postprocessing(points, start_near);
|
||||
|
||||
auto run_snake = [&points, start_near]() {
|
||||
std::vector<size_t> path = snake_core(points);
|
||||
if (start_near != nullptr && !path.empty()) {
|
||||
// Start the cycle at the point closest to start_near.
|
||||
size_t best_start = 0;
|
||||
double best_d2 = std::numeric_limits<double>::max();
|
||||
for (size_t k = 0; k < points.size(); ++k) {
|
||||
double d2 = (points[k].cast<double>() - start_near->cast<double>()).squaredNorm();
|
||||
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
|
||||
}
|
||||
auto it = std::find(path.begin(), path.end(), best_start);
|
||||
if (it != path.begin() && it != path.end())
|
||||
std::rotate(path.begin(), it, path.end());
|
||||
} else {
|
||||
tsp_rotate_minimize_closing(path, points);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
if (print_order == PrintOrder::Snake)
|
||||
return run_snake();
|
||||
|
||||
// Best-of: pick the shortest total cycle; tiebreak on smallest max edge.
|
||||
std::vector<std::vector<size_t>> candidates;
|
||||
candidates.emplace_back(chain_points_with_postprocessing(points, start_near));
|
||||
candidates.emplace_back(run_snake());
|
||||
|
||||
size_t best = 0;
|
||||
double best_len = std::numeric_limits<double>::max();
|
||||
double best_edge = std::numeric_limits<double>::max();
|
||||
for (size_t i = 0; i < candidates.size(); ++i) {
|
||||
double len = tsp_cycle_path_length(candidates[i], points);
|
||||
double edge = tsp_max_edge_length(candidates[i], points);
|
||||
if (len < best_len || (len == best_len && edge < best_edge)) {
|
||||
best_len = len; best_edge = edge; best = i;
|
||||
}
|
||||
}
|
||||
return candidates[best];
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,148 @@
|
||||
// Print-object ordering strategies and shared TSP post-processing utilities.
|
||||
|
||||
#ifndef slic3r_OrderingStrategies_hpp_
|
||||
#define slic3r_OrderingStrategies_hpp_
|
||||
|
||||
#include "../libslic3r.h"
|
||||
#include "../Point.hpp"
|
||||
|
||||
#ifndef SLIC3R_TEST_HARNESS
|
||||
#include "../Print.hpp"
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// --- Path improvement (operate on index vectors into `centers`) ---
|
||||
|
||||
// 2-opt improvement: reverses segments that reduce total cycle path length.
|
||||
// Returns true if any improvement was made.
|
||||
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes = 10);
|
||||
|
||||
// Crossing removal: reverse any segment pair whose edges geometrically cross.
|
||||
// Returns true if any crossing was removed.
|
||||
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
|
||||
|
||||
// Rotate the cycle so the closing edge (last -> first) is minimized.
|
||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
|
||||
|
||||
// Total Euclidean path length of a cycle (including closing edge).
|
||||
inline double tsp_cycle_path_length(const std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
if (path.size() < 2) return 0.0;
|
||||
double total = 0.0;
|
||||
for (size_t i = 0; i < path.size(); ++i) {
|
||||
size_t next = (i + 1) % path.size();
|
||||
total += (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Maximum edge length of a cycle (including closing edge).
|
||||
inline double tsp_max_edge_length(const std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
if (path.size() < 2) return 0.0;
|
||||
double mx = 0.0;
|
||||
for (size_t i = 0; i < path.size(); ++i) {
|
||||
size_t next = (i + 1) % path.size();
|
||||
double d = (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
|
||||
if (d > mx) mx = d;
|
||||
}
|
||||
return mx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#ifndef SLIC3R_TEST_HARNESS
|
||||
|
||||
// --- Wrapper boilerplate ---
|
||||
|
||||
// Collect instance centers from PrintObjects, optionally pre-rotate to honour
|
||||
// start_near, call a core algorithm, and map the result back to PrintInstance*.
|
||||
template<typename CoreFn>
|
||||
std::vector<const PrintInstance*> chain_instances_with_core(
|
||||
const std::vector<const PrintObject*>& print_objects,
|
||||
const Point* start_near,
|
||||
CoreFn&& core_fn)
|
||||
{
|
||||
Points instance_centers;
|
||||
std::vector<std::pair<size_t, size_t>> instances;
|
||||
for (size_t i = 0; i < print_objects.size(); ++i) {
|
||||
const PrintObject& object = *print_objects[i];
|
||||
for (size_t j = 0; j < object.instances().size(); ++j) {
|
||||
instance_centers.emplace_back(object.instances()[j].shift);
|
||||
instances.emplace_back(i, j);
|
||||
}
|
||||
}
|
||||
|
||||
if (instance_centers.empty()) return {};
|
||||
|
||||
// If start_near is provided, pre-rotate so closest point is first.
|
||||
if (start_near != nullptr) {
|
||||
size_t best_start = 0;
|
||||
double best_d2 = std::numeric_limits<double>::max();
|
||||
for (size_t k = 0; k < instance_centers.size(); ++k) {
|
||||
double d2 = (instance_centers[k].cast<double>() - start_near->cast<double>()).squaredNorm();
|
||||
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
|
||||
}
|
||||
std::rotate(instance_centers.begin(), instance_centers.begin() + best_start, instance_centers.end());
|
||||
std::rotate(instances.begin(), instances.begin() + best_start, instances.end());
|
||||
}
|
||||
|
||||
auto path = core_fn(instance_centers);
|
||||
|
||||
// Rotate the cycle so the first element is the best starting point.
|
||||
// When start_near is provided, pick the point closest to it (preserving
|
||||
// the pre-rotation). Otherwise minimise the closing edge.
|
||||
if (start_near != nullptr && !path.empty()) {
|
||||
// Pre-rotation already put the closest point at index 0.
|
||||
// Find where index 0 appears in the path and rotate it to the front.
|
||||
auto it = std::find(path.begin(), path.end(), size_t(0));
|
||||
if (it != path.begin())
|
||||
std::rotate(path.begin(), it, path.end());
|
||||
} else {
|
||||
tsp_rotate_minimize_closing(path, instance_centers);
|
||||
}
|
||||
|
||||
std::vector<const PrintInstance*> out;
|
||||
out.reserve(path.size());
|
||||
for (size_t step : path) {
|
||||
out.emplace_back(&print_objects[instances[step].first]->instances()[instances[step].second]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
#endif // SLIC3R_TEST_HARNESS
|
||||
|
||||
// --- Core algorithms (operate on raw Points, return index permutations) ---
|
||||
|
||||
// Snake ordering: row grouping + serpentine traversal + post-processing.
|
||||
std::vector<size_t> snake_core(const Points& centers);
|
||||
|
||||
#ifndef SLIC3R_TEST_HARNESS
|
||||
|
||||
// --- Production wrappers ---
|
||||
|
||||
// Snake ordering.
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print);
|
||||
|
||||
// Best-of-strategies: run all strategies and return the shortest result.
|
||||
// Primary: shortest total path; secondary tiebreaker: smallest max edge.
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
|
||||
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print);
|
||||
|
||||
// Order raw points with the selected strategy, returning an index permutation. Island-level
|
||||
// counterpart of the chain_print_object_instances_* helpers. The returned cycle starts at the
|
||||
// point closest to start_near; orders without a dedicated strategy use nearest-neighbor chaining.
|
||||
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near);
|
||||
|
||||
#endif // SLIC3R_TEST_HARNESS
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_OrderingStrategies_hpp_ */
|
||||
@@ -143,7 +143,8 @@ BoundingBoxf get_wipe_tower_extrusions_extents(const Print &print, const coordf_
|
||||
double wipe_tower_y = print.config().wipe_tower_y.get_at(plate_idx) + plate_origin(1);
|
||||
Transform2d trafo =
|
||||
Eigen::Translation2d(wipe_tower_x, wipe_tower_y) *
|
||||
Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value));
|
||||
Eigen::Rotation2Dd(Geometry::deg2rad(print.config().wipe_tower_rotation_angle.value)) *
|
||||
Eigen::Translation2d(print.wipe_tower_data().rib_offset.cast<double>()); // tower-local rib-wall shift, zero unless rib
|
||||
|
||||
BoundingBoxf bbox;
|
||||
for (const std::vector<WipeTower::ToolChangeResult> &tool_changes : print.wipe_tower_data().tool_changes) {
|
||||
|
||||
@@ -1091,6 +1091,16 @@ namespace Slic3r
|
||||
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1];
|
||||
std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf);
|
||||
|
||||
// Enable inter-layer forecast: when choosing filament ordering for current layer,
|
||||
// also consider next layer's filament set to minimize inter-layer transition flush.
|
||||
// solve_extruder_order_with_forcast() tries all permutations of curr+next layer
|
||||
// and picks the ordering that minimizes total flush across both layers.
|
||||
// This avoids expensive inter-layer transitions (e.g. ending layer with F2 when
|
||||
// next layer starts with F3, costing flush[F2→F3], instead of ending with F3
|
||||
// which gives flush[F3→F3]=0). Limited to ≤5 filaments due to O(N!×M!) complexity.
|
||||
// The per-nozzle base reorder does not use the inter-layer forecast. This function drives
|
||||
// BBL multi-extruder grouping cost and H2C ordering, so keeping it false avoids perturbing
|
||||
// existing H2D/H2C output.
|
||||
bool use_forcast = false;
|
||||
float tmp_cost = 0;
|
||||
std::vector<unsigned int> sequence;
|
||||
|
||||
+1369
-685
File diff suppressed because it is too large
Load Diff
+124
-101
@@ -12,7 +12,7 @@
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include <unordered_set>
|
||||
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
namespace Slic3r
|
||||
{
|
||||
|
||||
@@ -20,6 +20,17 @@ class WipeTowerWriter;
|
||||
class PrintConfig;
|
||||
enum GCodeFlavor : unsigned char;
|
||||
|
||||
// Cuts the tower wall polygon open at each skip point (a toolchange's entry position)
|
||||
// so the entry travel can pass through instead of crossing the printed wall. Defined in
|
||||
// WipeTower.cpp, shared by WipeTower and WipeTower2.
|
||||
Polylines construct_gap_for_skip_points(
|
||||
const Polygon& polygon, const std::vector<Vec2f>& skip_points, float wt_width, float gap_length, Polygon& insert_skip_polygon);
|
||||
|
||||
// Klipper acts on commands the instant it parses them, and its G4 reads only P (milliseconds),
|
||||
// so the zero-second and seconds-valued dwells every other flavor uses neither synchronize nor
|
||||
// pause there. Both defined in WipeTower.cpp, shared by WipeTower and WipeTower2.
|
||||
const char* flush_planner_queue_command(GCodeFlavor flavor); // finish queued moves, e.g. around M104/M109
|
||||
std::string wait_command(GCodeFlavor flavor, float seconds); // pause for `seconds`
|
||||
|
||||
class WipeTower
|
||||
{
|
||||
@@ -34,7 +45,11 @@ public:
|
||||
static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall);
|
||||
static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height);
|
||||
static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall);
|
||||
static Vec2f move_box_inside_box(const BoundingBox &box1, const BoundingBox &box2, int offset = 0);
|
||||
// Translation that brings a footprint inside the printable outline, padded by offset. The prime
|
||||
// tower is validated against the real outline (see layered_print_cleareance_valid), so clamping
|
||||
// against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons
|
||||
// must share one scaled coordinate frame; the translation comes back in millimeters.
|
||||
static Vec2f move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset = 0);
|
||||
static Polygon rounding_polygon(Polygon &polygon, double rounding = 2., double angle_tol = 30. / 180. * PI);
|
||||
struct Extrusion
|
||||
{
|
||||
@@ -58,6 +73,7 @@ public:
|
||||
Vec2f origin_start_pos; // not rotated
|
||||
|
||||
std::vector<Vec2f> wipe_path;
|
||||
bool is_extruder_change{true};
|
||||
};
|
||||
|
||||
struct ToolChangeResult
|
||||
@@ -83,7 +99,6 @@ public:
|
||||
bool priming;
|
||||
|
||||
bool is_tool_change{false};
|
||||
bool is_contact{false};
|
||||
Vec2f tool_change_start_pos;
|
||||
|
||||
// Pass a polyline so that normal G-code generator can do a wipe for us.
|
||||
@@ -107,6 +122,7 @@ public:
|
||||
// executing the gcode finish_layer_tcr.
|
||||
bool is_finish_first = false;
|
||||
|
||||
bool is_contact = false;
|
||||
NozzleChangeResult nozzle_change_result;
|
||||
|
||||
// Sum the total length of the extrusion.
|
||||
@@ -121,6 +137,8 @@ public:
|
||||
}
|
||||
return e_length;
|
||||
}
|
||||
// Orca: set by WipeTower2 (non-BBL tower) to force a travel to the tower even when the
|
||||
// previous position is unknown; read by WipeTowerIntegration::append_tcr2 (GCode.cpp).
|
||||
bool force_travel = false;
|
||||
};
|
||||
|
||||
@@ -161,15 +179,12 @@ public:
|
||||
bool priming,
|
||||
size_t old_tool,
|
||||
bool is_finish,
|
||||
bool is_tool_change,
|
||||
float purge_volume,
|
||||
bool is_contact = false) const;
|
||||
bool is_tool_change, float purge_volume, bool is_contact) const;
|
||||
|
||||
ToolChangeResult construct_block_tcr(WipeTowerWriter& writer,
|
||||
bool priming,
|
||||
size_t filament_id,
|
||||
bool is_finish,
|
||||
float purge_volume) const;
|
||||
bool is_finish, float purge_volume) const;
|
||||
|
||||
|
||||
// x -- x coordinates of wipe tower in mm ( left bottom corner )
|
||||
@@ -183,9 +198,14 @@ public:
|
||||
// Set the extruder properties.
|
||||
void set_extruder(size_t idx, const PrintConfig& config);
|
||||
|
||||
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
|
||||
// Orca: has_filament_switcher is not a static PrintConfig member here, so it is pushed in from
|
||||
// Print via a setter rather than read in the ctor. Device-set only.
|
||||
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
|
||||
// Appends into internal structure m_plan containing info about the future wipe tower
|
||||
// to be used before building begins. The entries must be added ordered in z.
|
||||
void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume = 0.f, float prime_volume = 0.f);
|
||||
void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume_ec = 0.f, float wipe_volume_nc = 0.f, float prime_volume = 0.f);
|
||||
|
||||
|
||||
// Iterates through prepared m_plan, generates ToolChangeResults and appends them to "result"
|
||||
void generate(std::vector<std::vector<ToolChangeResult>> &result);
|
||||
@@ -218,9 +238,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void set_wipe_volume(std::vector<std::vector<float>>& wiping_matrix) {
|
||||
wipe_volumes = wiping_matrix;
|
||||
}
|
||||
|
||||
// Switch to a next layer.
|
||||
void set_layer(
|
||||
@@ -249,7 +266,6 @@ public:
|
||||
|
||||
// Calculate extrusion flow from desired line width, nozzle diameter, filament diameter and layer_height:
|
||||
m_extrusion_flow = extrusion_flow(layer_height);
|
||||
|
||||
// Advance m_layer_info iterator, making sure we got it right
|
||||
while (!m_plan.empty() && m_layer_info->z < print_z - WT_EPSILON && m_layer_info+1 != m_plan.end())
|
||||
++m_layer_info;
|
||||
@@ -308,18 +324,9 @@ public:
|
||||
std::vector<float> get_used_filament() const { return m_used_filament_length; }
|
||||
int get_number_of_toolchanges() const { return m_num_tool_changes; }
|
||||
|
||||
void set_filament_map(const std::vector<int> &filament_map) { m_filament_map = filament_map; }
|
||||
|
||||
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
|
||||
|
||||
bool has_tpu_filament() const { return m_has_tpu_filament; }
|
||||
|
||||
// Orca: has_filament_switcher is not a static PrintConfig member, so it is pushed in from Print
|
||||
// via a setter rather than read in the ctor. Device-set only.
|
||||
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
|
||||
// The region every extruder can reach, used to clamp the PETG pre-extrusion offset to the
|
||||
// printable bed.
|
||||
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
|
||||
|
||||
struct FilamentParameters {
|
||||
std::string material = "PLA";
|
||||
int category;
|
||||
@@ -328,15 +335,15 @@ public:
|
||||
bool is_support = false;
|
||||
int nozzle_temperature = 0;
|
||||
int nozzle_temperature_initial_layer = 0;
|
||||
int interface_print_temperature = 0;
|
||||
float loading_speed = 0.f;
|
||||
float loading_speed_start = 0.f;
|
||||
float unloading_speed = 0.f;
|
||||
float unloading_speed_start = 0.f;
|
||||
float delay = 0.f ;
|
||||
int cooling_moves = 0;
|
||||
float cooling_initial_speed = 0.f;
|
||||
float cooling_final_speed = 0.f;
|
||||
// BBS: remove useless config
|
||||
//float loading_speed = 0.f;
|
||||
//float loading_speed_start = 0.f;
|
||||
//float unloading_speed = 0.f;
|
||||
//float unloading_speed_start = 0.f;
|
||||
//float delay = 0.f ;
|
||||
//int cooling_moves = 0;
|
||||
//float cooling_initial_speed = 0.f;
|
||||
//float cooling_final_speed = 0.f;
|
||||
float ramming_line_width_multiplicator = 1.f;
|
||||
float ramming_step_multiplicator = 1.f;
|
||||
float max_e_speed = std::numeric_limits<float>::max();
|
||||
@@ -346,35 +353,41 @@ public:
|
||||
float retract_length;
|
||||
float retract_speed;
|
||||
float wipe_dist;
|
||||
float tower_interface_pre_extrusion_dist = 0.f;
|
||||
float tower_interface_pre_extrusion_length = 0.f;
|
||||
// Outward shift of the wipe start for a PETG pre-extrusion on filament-switcher devices;
|
||||
// set from filament_tower_interface_pre_extrusion_dist.
|
||||
float petg_pre_extrusion_offset_dist = 0.f;
|
||||
float tower_ironing_area = 4.f;
|
||||
float tower_interface_purge_length = 0.f;
|
||||
// Distance (in mm of filament) that a hotend is allowed to pre-cool before the
|
||||
// tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
|
||||
float filament_cooling_before_tower = 0.f;
|
||||
std::pair<float,float> max_e_ramming_speed;//[0]extruder change [1]nozzle change
|
||||
std::pair<float, float> ramming_travel_time; // Travel time after ramming
|
||||
std::pair<std::vector<float>,std::vector<float>> precool_t;//Pre-cooling time, set to 0 to ensure the ramming speed is controlled solely by ramming volumetric speed.
|
||||
std::pair<std::vector<float>, std::vector<float>> precool_t_first_layer;
|
||||
std::pair<int,int> precool_target_temp;
|
||||
float filament_cooling_before_tower = 0.f;
|
||||
float flat_iron_area;
|
||||
float filament_tower_interface_print_temp;
|
||||
float filament_tower_interface_pre_extrusion_dist = 0;
|
||||
float filament_tower_interface_pre_extrusion_length = 0;
|
||||
float filament_petg_pre_extrusion_offset_dist = 0;
|
||||
};
|
||||
|
||||
|
||||
void set_used_filament_ids(const std::vector<int> &used_filament_ids) { m_used_filament_ids = used_filament_ids; };
|
||||
void set_used_filament_ids(const std::vector<int> &used_filament_ids) { m_used_filament_ids = used_filament_ids; };
|
||||
void set_filament_categories(const std::vector<int> & filament_categories) { m_filament_categories = filament_categories;};
|
||||
std::vector<int> m_used_filament_ids;
|
||||
void set_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult &multi_nozzle_group_result) { m_multi_nozzle_group_result = &multi_nozzle_group_result; };
|
||||
std::vector<int> m_used_filament_ids;
|
||||
std::vector<int> m_filament_categories;
|
||||
const MultiNozzleUtils::LayeredNozzleGroupResult *m_multi_nozzle_group_result{nullptr};
|
||||
|
||||
enum class WipeTowerLayerType : unsigned char { Normal, Contact, Solid, Contact_UP};// Contact layer should be solid and reduce feed
|
||||
|
||||
struct WipeTowerBlock
|
||||
{
|
||||
int block_id{0};
|
||||
int filament_adhesiveness_category{0};
|
||||
std::vector<float> layer_depths;
|
||||
std::vector<bool> solid_infill;
|
||||
//std::vector<bool> solid_infill;
|
||||
std::vector<float> finish_depth{0}; // the start pos of finish frame for every layer
|
||||
std::vector<WipeTowerLayerType> layers_type; // type of the layer, normal, Contact or Solid
|
||||
float depth{0};
|
||||
float start_depth{0};
|
||||
float cur_depth{0};
|
||||
int last_filament_change_id{-1};
|
||||
int last_filament_change_id{-1};
|
||||
int last_nozzle_change_id{-1};
|
||||
};
|
||||
|
||||
@@ -394,23 +407,33 @@ public:
|
||||
WipeTowerBlock* get_block_by_category(int filament_adhesiveness_category, bool create);
|
||||
void add_depth_to_block(int filament_id, int filament_adhesiveness_category, float depth, bool is_nozzle_change = false);
|
||||
int get_filament_category(int filament_id);
|
||||
bool is_in_same_extruder(int filament_id_1, int filament_id_2);
|
||||
void reset_block_status();
|
||||
int get_wall_filament_for_all_layer();
|
||||
// for generate new wipe tower
|
||||
void generate_new(std::vector<std::vector<WipeTower::ToolChangeResult>> &result);
|
||||
|
||||
void plan_tower_new();
|
||||
void generate_wipe_tower_blocks();
|
||||
void generate_wipe_tower_blocks(bool add_solid_flag);
|
||||
void update_all_layer_depth(float wipe_tower_depth);
|
||||
|
||||
void set_nozzle_last_layer_id();
|
||||
void set_first_layer_flow_ratio(const float flow_ratio);
|
||||
// Orca: default/initial-layer/travel acceleration are object-scope options here (PrintConfig
|
||||
// members in BBS), so Print pushes the resolved per-variant columns in via this setter.
|
||||
void set_accelerations(const std::vector<double> &normal, const std::vector<double> &first_layer_normal,
|
||||
const std::vector<double> &travel, const std::vector<double> &first_layer_travel);
|
||||
void calc_block_infill_gap();
|
||||
ToolChangeResult tool_change_new(size_t new_tool, bool solid_change = false, bool solid_nozzlechange=false);
|
||||
NozzleChangeResult nozzle_change_new(int old_filament_id, int new_filament_id, bool solid_change = false);
|
||||
NozzleChangeResult ramming(int old_filament_id, int new_filament_id, bool solid_change = false, bool extruder_change = true); // extruder_chang means nozzle_change
|
||||
ToolChangeResult finish_layer_new(bool extrude_perimeter = true, bool extrude_fill = true, bool extrude_fill_wall = true);
|
||||
ToolChangeResult finish_block(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true);
|
||||
ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true ,bool interface_solid =false);
|
||||
ToolChangeResult finish_block_solid(const WipeTowerBlock &block, int filament_id, bool extrude_fill = true, WipeTowerLayerType layer_type = WipeTowerLayerType::Normal);
|
||||
void toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinates &cleaning_box, float wipe_length,bool solid_toolchange=false);
|
||||
Vec2f get_rib_offset() const { return m_rib_offset; }
|
||||
bool is_need_ramming(int filament_id_1, int filament_id_2, int layer_id) const;
|
||||
bool is_same_extruder(int filament_id_1, int filament_id_2, int layer_id) const;
|
||||
bool is_same_nozzle(int filament_id_1, int filament_id_2, int layer_id) const;
|
||||
int get_nozzle_id(int filament_id, int layer_id) const;
|
||||
int get_extruder_id(int filament_id, int layer_id) const;
|
||||
|
||||
private:
|
||||
enum wipe_shape // A fill-in direction
|
||||
@@ -430,7 +453,6 @@ private:
|
||||
bool m_enable_wrapping_detection = false;
|
||||
bool m_enable_timelapse_print = false;
|
||||
bool m_semm = true; // Are we using a single extruder multimaterial printer?
|
||||
bool m_purge_in_prime_tower = false; // Do we purge in the prime tower?
|
||||
Vec2f m_wipe_tower_pos; // Left front corner of the wipe tower in mm.
|
||||
float m_wipe_tower_width; // Width of the wipe tower.
|
||||
float m_wipe_tower_depth = 0.f; // Depth of the wipe tower
|
||||
@@ -448,11 +470,11 @@ private:
|
||||
float m_travel_speed = 0.f;
|
||||
float m_first_layer_speed = 0.f;
|
||||
size_t m_first_layer_idx = size_t(-1);
|
||||
|
||||
std::vector<double> m_filaments_change_length;
|
||||
Vec2f m_origin;
|
||||
std::vector<int> m_last_layer_id;
|
||||
std::pair<std::vector<double>,std::vector<double>> m_filaments_change_length;//[0]extruder change [1]nozzle change
|
||||
size_t m_cur_layer_id;
|
||||
NozzleChangeResult m_nozzle_change_result;
|
||||
std::vector<int> m_filament_map;
|
||||
bool m_has_tpu_filament{false};
|
||||
bool m_is_multi_extruder{false};
|
||||
bool m_use_gap_wall{false};
|
||||
@@ -463,33 +485,32 @@ private:
|
||||
bool m_used_fillet{false};
|
||||
Vec2f m_rib_offset{Vec2f(0.f, 0.f)};
|
||||
bool m_tower_framework{false};
|
||||
|
||||
bool m_need_reverse_travel{false};
|
||||
bool m_enable_tower_interface_features{false};
|
||||
// G-code generator parameters.
|
||||
float m_cooling_tube_retraction = 0.f;
|
||||
float m_cooling_tube_length = 0.f;
|
||||
float m_parking_pos_retraction = 0.f;
|
||||
float m_extra_loading_move = 0.f;
|
||||
// BBS: remove useless config
|
||||
//float m_cooling_tube_retraction = 0.f;
|
||||
//float m_cooling_tube_length = 0.f;
|
||||
//float m_parking_pos_retraction = 0.f;
|
||||
//float m_extra_loading_move = 0.f;
|
||||
float m_bridging = 0.f;
|
||||
bool m_no_sparse_layers = false;
|
||||
bool m_set_extruder_trimpot = false;
|
||||
// BBS: remove useless config
|
||||
//bool m_set_extruder_trimpot = false;
|
||||
bool m_adhesion = true;
|
||||
GCodeFlavor m_gcode_flavor;
|
||||
|
||||
// Multi-nozzle prime-tower heating during wipe. m_is_multiple_nozzle gates the whole
|
||||
// feature; it is false for every current (single-nozzle) printer (extruder_max_nozzle_count
|
||||
// defaults to 1), so the pre-heat/pre-cool path is inert and wipe-tower g-code is unchanged.
|
||||
bool m_is_multiple_nozzle = false;
|
||||
std::vector<double> m_hotend_heating_rate; // config.hotend_heating_rate (deg/s per extruder)
|
||||
std::vector<int> m_physical_extruder_map; // logical extruder -> physical tool number (M104 T param)
|
||||
|
||||
// Per-extruder printable-height clamp. m_printable_height = config.extruder_printable_height
|
||||
// (per-extruder Z limit; empty for single-extruder printers, [320,325] for H2D). m_last_layer_id
|
||||
// records, per extruder, the last wipe-tower layer that uses it. is_valid_last_layer() is gated on
|
||||
// m_is_multi_extruder so single-extruder wipe-tower g-code is unchanged; the clamp only bites a
|
||||
// multi-extruder wipe tower whose final per-extruder layer exceeds that extruder's printable
|
||||
// height (near the Z limit).
|
||||
std::vector<double> m_printable_height;
|
||||
std::vector<int> m_last_layer_id;
|
||||
bool m_is_multiple_nozzle = false;
|
||||
std::vector<unsigned int> m_normal_accels;
|
||||
std::vector<unsigned int> m_first_layer_normal_accels;
|
||||
std::vector<unsigned int> m_travel_accels;
|
||||
std::vector<unsigned int> m_first_layer_travel_accels;
|
||||
unsigned int m_max_accels;
|
||||
bool m_accel_to_decel_enable;
|
||||
float m_accel_to_decel_factor;
|
||||
bool m_enable_arc_fitting = true;
|
||||
std::vector<double> m_hotend_heating_rate;
|
||||
std::vector<double> m_hotend_cooling_rate;
|
||||
Polygons m_shared_print_bed;
|
||||
|
||||
// Bed properties
|
||||
enum {
|
||||
@@ -500,10 +521,11 @@ private:
|
||||
float m_bed_width; // width of the bed bounding box
|
||||
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
|
||||
|
||||
float m_first_layer_flow_ratio;
|
||||
float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill.
|
||||
float m_nozzle_change_perimeter_width = 0.4f * Width_To_Nozzle_Ratio;
|
||||
float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter.
|
||||
|
||||
std::unordered_map<int, std::pair<float,float>> m_block_infill_gap_width; // categories to infill_gap: toolchange gap, nozzlechange gap
|
||||
// Extruder specific parameters.
|
||||
std::vector<FilamentParameters> m_filpar;
|
||||
|
||||
@@ -516,49 +538,52 @@ private:
|
||||
// A fill-in direction (positive Y, negative Y) alternates with each layer.
|
||||
wipe_shape m_current_shape = SHAPE_NORMAL;
|
||||
size_t m_current_tool = 0;
|
||||
// Orca: support mmu wipe tower
|
||||
std::vector<std::vector<float>> wipe_volumes;
|
||||
// BBS
|
||||
//const std::vector<std::vector<float>> wipe_volumes;
|
||||
|
||||
float m_depth_traversed = 0.f; // Current y position at the wipe tower.
|
||||
bool m_current_layer_finished = false;
|
||||
bool m_left_to_right = true;
|
||||
float m_extra_spacing = 1.f;
|
||||
float m_tpu_fixed_spacing = 2;
|
||||
std::vector<Vec2f> m_wall_skip_points;
|
||||
float m_max_speed = 5400.f; // the maximum printing speed on the prime tower.
|
||||
std::vector<std::vector<Vec2f>> m_wall_skip_points;
|
||||
std::map<float,Polylines> m_outer_wall;
|
||||
std::vector<double> m_printable_height;
|
||||
bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; }
|
||||
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
|
||||
bool m_flat_ironing=false;
|
||||
bool m_enable_tower_interface_features=false;
|
||||
bool m_enable_tower_interface_cooldown_during_tower=false;
|
||||
// Filament-switcher device flag + shared printable bed for the PETG pre-extrusion offset.
|
||||
// m_has_filament_switcher is false for the whole shipping fleet (no profile sets the key), so
|
||||
// the PETG branch in get_next_pos never runs -> no change fleet-wide.
|
||||
bool m_has_filament_switcher=false;
|
||||
Polygons m_shared_print_bed;
|
||||
bool m_prev_layer_had_interface=false;
|
||||
bool m_current_layer_has_interface=false;
|
||||
bool m_contact_ironing = false;
|
||||
bool m_has_filament_switcher = false;
|
||||
float m_contact_speed = 20 * 60.f;
|
||||
std::vector<int> m_physical_extruder_map;
|
||||
// Calculates length of extrusion line to extrude given volume
|
||||
float volume_to_length(float volume, float line_width, float layer_height) const {
|
||||
return std::max(0.f, volume / (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f))));
|
||||
}
|
||||
|
||||
// Calculates volume of extrusion line
|
||||
float length_to_volume(float length,float line_width, float layer_height) const
|
||||
{
|
||||
return std::max(0.f, length * (layer_height * (line_width - layer_height * (1.f - float(M_PI) / 4.f))));
|
||||
}
|
||||
// Calculates depth for all layers and propagates them downwards
|
||||
void plan_tower();
|
||||
|
||||
// Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental
|
||||
void make_wipe_tower_square();
|
||||
|
||||
Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool interface_layer, size_t interface_tool);
|
||||
Vec2f get_next_pos(const WipeTower::box_coordinates &cleaning_box, float wipe_length, bool solid_toolchange);
|
||||
|
||||
// Goes through m_plan, calculates border and finish_layer extrusions and subtracts them from last wipe
|
||||
void save_on_last_wipe();
|
||||
|
||||
bool is_tpu_filament(int filament_id) const;
|
||||
bool is_petg_filament(int filament_id) const;
|
||||
|
||||
bool is_need_reverse_travel(int filament, bool extruder_change) const;
|
||||
// BBS
|
||||
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
|
||||
|
||||
void set_for_wipe_tower_writer(WipeTowerWriter &writer);
|
||||
|
||||
// to store information about tool changes for a given layer
|
||||
struct WipeTowerInfo{
|
||||
@@ -571,6 +596,7 @@ private:
|
||||
float wipe_volume;
|
||||
float wipe_length;
|
||||
float nozzle_change_depth{0};
|
||||
float nozzle_change_length{0};
|
||||
// BBS
|
||||
float purge_volume;
|
||||
ToolChange(size_t old, size_t newtool, float depth=0.f, float ramming_depth=0.f, float fwl=0.f, float wv=0.f, float wl = 0, float pv = 0)
|
||||
@@ -600,7 +626,7 @@ private:
|
||||
// ot -1 if there is no such toolchange.
|
||||
int first_toolchange_to_nonsoluble_nonsupport(
|
||||
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const;
|
||||
|
||||
WipeTowerInfo::ToolChange set_toolchange(int old_tool, int new_tool, float layer_height, float wipe_volume, float purge_volume,int layer_id);
|
||||
void toolchange_Unload(
|
||||
WipeTowerWriter &writer,
|
||||
const box_coordinates &cleaning_box,
|
||||
@@ -620,13 +646,10 @@ private:
|
||||
WipeTowerWriter &writer,
|
||||
const box_coordinates &cleaning_box,
|
||||
float wipe_volume);
|
||||
void get_wall_skip_points(const WipeTowerInfo &layer);
|
||||
|
||||
// Per-extruder printable-height clamp (see m_printable_height). is_valid_last_layer returns
|
||||
// false only for a multi-extruder wipe tower's final per-extruder layer that exceeds that
|
||||
// extruder's printable height; returns true (no clamp) in every other case.
|
||||
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
|
||||
void set_nozzle_last_layer_id();
|
||||
void get_wall_skip_points(const WipeTowerInfo &layer,int layer_id);
|
||||
void get_all_wall_skip_points();
|
||||
ToolChangeResult merge_tcr(ToolChangeResult &first, ToolChangeResult &second);
|
||||
float get_block_gap_width(int tool, bool is_nozzlechangle = false);
|
||||
};
|
||||
|
||||
|
||||
|
||||
+431
-345
File diff suppressed because it is too large
Load Diff
@@ -17,13 +17,20 @@ namespace Slic3r
|
||||
|
||||
class WipeTowerWriter2;
|
||||
class PrintRegionConfig;
|
||||
class ConfigBase;
|
||||
|
||||
class WipeTower2
|
||||
{
|
||||
public:
|
||||
static const std::string never_skip_tag() { return "_GCODE_WIPE_TOWER_NEVER_SKIP_TAG"; }
|
||||
// Marks the wait-for-temp-on-wipe-tower M109 so the interface-temp deduplication pass
|
||||
// in WipeTowerIntegration::append_tcr2 does not strip it.
|
||||
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
|
||||
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
|
||||
static std::vector<std::vector<float>> extract_wipe_volumes(const PrintConfig& config);
|
||||
static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
|
||||
// Estimated total flush volume of a SEMM print with the given number of filaments,
|
||||
// used to reserve wipe tower space before the tower is generated.
|
||||
static float estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt);
|
||||
|
||||
|
||||
// Construct ToolChangeResult from current state of WipeTower2 and WipeTowerWriter2.
|
||||
@@ -34,6 +41,15 @@ public:
|
||||
bool is_finish,
|
||||
bool is_contact = false) const;
|
||||
|
||||
// Whether this print cuts wall openings ("skip points") at the toolchange entries.
|
||||
// Shared with the entry routing in GCode.cpp so the router and the tower agree.
|
||||
static bool use_gap_wall(const PrintConfig& config);
|
||||
|
||||
// Whether the blocking toolchange temperature wait moves onto the wipe tower.
|
||||
// Shared with the defer flag in GCode.cpp append_tcr2 so the deferral and the
|
||||
// tower's tagged M109 can never disagree.
|
||||
static bool wait_for_temp_enabled(const PrintConfig& config);
|
||||
|
||||
// x -- x coordinates of wipe tower in mm ( left bottom corner )
|
||||
// y -- y coordinates of wipe tower in mm ( left bottom corner )
|
||||
// width -- width of wipe tower in mm ( default 60 mm - leave as it is )
|
||||
@@ -69,9 +85,9 @@ public:
|
||||
const float brim = m_wipe_tower_brim_width_real;
|
||||
return BoundingBoxf(Vec2d(-brim, -brim), Vec2d(double(m_wipe_tower_width) + brim, double(m_wipe_tower_depth) + brim));
|
||||
}
|
||||
// WT2 doesn't currently compute a rib-origin compensation like WipeTower (m_rib_offset),
|
||||
// so expose a zero offset for consistency purposes (to maintain API parity).
|
||||
Vec2f get_rib_offset() const { return Vec2f::Zero(); }
|
||||
// Tower-local shift that puts the rib wall's first-layer min corner at the configured
|
||||
// tower position, like WipeTower::get_rib_offset(). Zero unless the rib wall is used.
|
||||
Vec2f get_rib_offset() const { return m_rib_offset; }
|
||||
float get_rib_width() const { return m_rib_width; }
|
||||
float get_rib_length() const { return m_rib_length; }
|
||||
|
||||
@@ -149,6 +165,7 @@ public:
|
||||
struct FilamentParameters {
|
||||
std::string material = "PLA";
|
||||
bool is_soluble = false;
|
||||
bool is_support = false;
|
||||
int temperature = 0;
|
||||
int first_layer_temperature = 0;
|
||||
int interface_print_temperature = 0;
|
||||
@@ -220,9 +237,9 @@ private:
|
||||
float m_perimeter_speed = 0.f;
|
||||
float m_first_layer_speed = 0.f;
|
||||
size_t m_first_layer_idx = size_t(-1);
|
||||
bool m_flat_ironing = false;
|
||||
bool m_enable_tower_interface_features = false;
|
||||
bool m_enable_tower_interface_cooldown_during_tower = false;
|
||||
bool m_wait_for_temp_on_wipe_tower = false;
|
||||
bool m_prev_layer_had_interface = false;
|
||||
bool m_current_layer_has_interface = false;
|
||||
|
||||
@@ -231,6 +248,12 @@ private:
|
||||
float m_rib_width = 10;
|
||||
float m_extra_rib_length = 0;
|
||||
float m_rib_length = 0;
|
||||
Vec2f m_rib_offset = Vec2f::Zero();
|
||||
bool m_use_gap_wall = false;
|
||||
// Per plan layer, each toolchange's entry position (tower-local, un-shifted frame):
|
||||
// where the wall is cut open so the entry travel does not cross the printed wall.
|
||||
// Filled by compute_wall_skip_points() once the plan is final.
|
||||
std::vector<std::vector<Vec2f>> m_wall_skip_points;
|
||||
|
||||
bool m_enable_arc_fitting = false;
|
||||
|
||||
@@ -253,6 +276,7 @@ private:
|
||||
} m_bed_shape;
|
||||
float m_bed_width; // width of the bed bounding box
|
||||
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
|
||||
Polygon m_bed_polygon; // printable_area contour (scaled)
|
||||
|
||||
float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill.
|
||||
float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter.
|
||||
@@ -278,6 +302,37 @@ private:
|
||||
|
||||
bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; }
|
||||
|
||||
// Purge row lattice of toolchange_Wipe(): row pitch and extrusion width.
|
||||
float wipe_row_spacing(bool first_layer) const { return (first_layer ? m_extra_flow : m_extra_spacing_wipe) * m_perimeter_width; }
|
||||
float wipe_line_width() const { return m_perimeter_width * m_extra_flow; }
|
||||
|
||||
// Whether toolchange_Unload() rams this (old) tool out.
|
||||
bool tool_ramming_enabled(size_t tool) const { return (m_semm && m_enable_filament_ramming) || m_filpar[tool].multitool_ramming; }
|
||||
// Whether the wipe restarts at the box boundary on a fresh row below the quantized
|
||||
// ram band after ramming this (old) tool out (multi-tool gap wall; SEMM keeps the
|
||||
// stock continue-from-ram-end behavior).
|
||||
bool boundary_wipe_start_enabled(size_t tool) const { return tool_ramming_enabled(tool) && !m_semm && m_use_gap_wall; }
|
||||
|
||||
// With a boundary wipe start the wipe begins on a fresh row below the quantized ram
|
||||
// band. Y offset from the box start to that first wipe row.
|
||||
float wipe_start_offset_after_ram(float ramming_depth, bool first_layer) const
|
||||
{
|
||||
return ramming_depth + wipe_row_spacing(first_layer) - (m_perimeter_width + wipe_line_width()) / 2.f;
|
||||
}
|
||||
|
||||
// Tower-local entry position of a toolchange whose box starts depth_traversed into
|
||||
// the layer: the box corner, moved down to the first wipe row when the plan gives
|
||||
// it a boundary wipe start (ramming_depth > 0 iff the unload rams). tool_change()
|
||||
// enters here and compute_wall_skip_points() cuts the wall gap here, so the routed
|
||||
// entry, the gap and the wipe scrub all share one opening.
|
||||
Vec2f toolchange_entry_pos(float depth_traversed, float ramming_depth, bool first_layer) const
|
||||
{
|
||||
Vec2f pos(m_perimeter_width / 2.f, m_perimeter_width / 2.f + depth_traversed);
|
||||
if (!m_semm && m_use_gap_wall && ramming_depth > 0.f)
|
||||
pos.y() += wipe_start_offset_after_ram(ramming_depth, first_layer);
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Calculates extrusion flow needed to produce required line width for given layer height
|
||||
float extrusion_flow(float layer_height = -1.f) const // negative layer_height - return current m_extrusion_flow
|
||||
{
|
||||
@@ -328,9 +383,10 @@ private:
|
||||
std::vector<float> m_used_filament_length;
|
||||
std::vector<std::pair<float, std::vector<float>>> m_used_filament_length_until_layer;
|
||||
|
||||
// Return index of first toolchange that switches to non-soluble extruder
|
||||
// ot -1 if there is no such toolchange.
|
||||
int first_toolchange_to_nonsoluble(
|
||||
// Return the index of the toolchange whose new filament should print the layer's
|
||||
// finish extrusions (sparse infill + wall + brim), or -1 to print them with the
|
||||
// layer's incoming filament before any toolchange happens.
|
||||
int first_toolchange_to_nonsoluble_nonsupport(
|
||||
const std::vector<WipeTowerInfo::ToolChange>& tool_changes) const;
|
||||
|
||||
void toolchange_Unload(
|
||||
@@ -343,7 +399,9 @@ private:
|
||||
void toolchange_Change(
|
||||
WipeTowerWriter2 &writer,
|
||||
const size_t new_tool,
|
||||
const std::string& new_material);
|
||||
const std::string& new_material,
|
||||
const int wait_for_temp,
|
||||
const bool wait_beside_tower);
|
||||
|
||||
void toolchange_Load(
|
||||
WipeTowerWriter2 &writer,
|
||||
@@ -353,7 +411,9 @@ private:
|
||||
WipeTowerWriter2 &writer,
|
||||
const WipeTower::box_coordinates &cleaning_box,
|
||||
float wipe_volume,
|
||||
bool interface_layer);
|
||||
bool interface_layer,
|
||||
bool priming = false,
|
||||
bool fill_box = false);
|
||||
|
||||
|
||||
Polygon generate_support_rib_wall(WipeTowerWriter2& writer,
|
||||
@@ -361,8 +421,7 @@ private:
|
||||
double feedrate,
|
||||
bool first_layer,
|
||||
bool rib_wall,
|
||||
bool extrude_perimeter,
|
||||
bool skip_points);
|
||||
bool extrude_perimeter);
|
||||
|
||||
Polygon generate_support_cone_wall(
|
||||
WipeTowerWriter2& writer,
|
||||
@@ -372,6 +431,12 @@ private:
|
||||
float spacing);
|
||||
|
||||
Polygon generate_rib_polygon(const WipeTower::box_coordinates& wt_box);
|
||||
|
||||
void compute_wall_skip_points();
|
||||
|
||||
// Computes the depth reserved for a toolchange (shared by plan_toolchange() and the
|
||||
// rib-wall square-tower replanning in generate()).
|
||||
WipeTowerInfo::ToolChange set_toolchange(size_t old_tool, size_t new_tool, float layer_height, float wipe_volume, bool first_layer_plan);
|
||||
};
|
||||
|
||||
|
||||
|
||||
+126
-29
@@ -2,6 +2,9 @@
|
||||
#include "CustomGCode.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Geometry/ArcWelder.hpp"
|
||||
#include "Line.hpp"
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
@@ -99,9 +102,87 @@ void GCodeWriter::apply_print_config(const PrintConfig &print_config)
|
||||
m_max_jerk_z = LIMITS(machine_max_jerk_z);
|
||||
m_max_jerk_e = LIMITS(machine_max_jerk_e);
|
||||
m_resolution = print_config.resolution.value;
|
||||
|
||||
#undef LIMITS
|
||||
#undef LIMITS_UINT
|
||||
// Orca: capture the printable area(s) so a spiral lift can be skipped when its
|
||||
// circle would leave the boundary and collide with the print limits. Full polygons
|
||||
// are stored (not a bounding box) so the check stays correct for non-rectangular
|
||||
// beds, and per-extruder areas are kept so printers with different boundaries per
|
||||
// extruder use the right limit for whichever extruder is active.
|
||||
auto to_scaled_polygon = [](const Pointfs &pts) {
|
||||
Polygon poly;
|
||||
poly.points.reserve(pts.size());
|
||||
for (const Vec2d &p : pts)
|
||||
poly.points.emplace_back(coord_t(scale_(p.x())), coord_t(scale_(p.y())));
|
||||
poly.make_counter_clockwise();
|
||||
return poly;
|
||||
};
|
||||
|
||||
m_bed_printable_area.points.clear();
|
||||
m_extruder_printable_areas.clear();
|
||||
|
||||
if (print_config.printable_area.values.size() >= 3)
|
||||
m_bed_printable_area = to_scaled_polygon(print_config.printable_area.values);
|
||||
|
||||
const std::vector<Pointfs> &extruder_areas = print_config.extruder_printable_area.values;
|
||||
if (!extruder_areas.empty()) {
|
||||
m_extruder_printable_areas.resize(extruder_areas.size());
|
||||
for (size_t i = 0; i < extruder_areas.size(); ++i) {
|
||||
if (extruder_areas[i].size() < 3) {
|
||||
// No dedicated area for this extruder: it can reach the whole bed.
|
||||
m_extruder_printable_areas[i] = m_bed_printable_area;
|
||||
continue;
|
||||
}
|
||||
Polygon extruder_poly = to_scaled_polygon(extruder_areas[i]);
|
||||
if (m_bed_printable_area.points.size() < 3) {
|
||||
m_extruder_printable_areas[i] = std::move(extruder_poly);
|
||||
continue;
|
||||
}
|
||||
// The reachable area is the extruder area clipped to the bed. Bed shapes are
|
||||
// convex in practice, so keep the largest resulting contour.
|
||||
Polygons clipped = intersection(extruder_poly, m_bed_printable_area);
|
||||
const Polygon *largest = nullptr;
|
||||
double best_area = 0.;
|
||||
for (const Polygon &p : clipped) {
|
||||
double a = std::abs(p.area());
|
||||
if (a > best_area) { best_area = a; largest = &p; }
|
||||
}
|
||||
m_extruder_printable_areas[i] = largest ? *largest : std::move(extruder_poly);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Polygon *GCodeWriter::active_printable_area() const
|
||||
{
|
||||
if (const Extruder *e = this->filament()) {
|
||||
size_t id = e->extruder_id();
|
||||
if (id < m_extruder_printable_areas.size() && m_extruder_printable_areas[id].points.size() >= 3)
|
||||
return &m_extruder_printable_areas[id];
|
||||
}
|
||||
if (m_bed_printable_area.points.size() >= 3)
|
||||
return &m_bed_printable_area;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool GCodeWriter::spiral_lift_fits_printable_area(const Vec2d ¢er, double radius) const
|
||||
{
|
||||
const Polygon *area = this->active_printable_area();
|
||||
if (area == nullptr)
|
||||
return true; // Boundary unknown: don't restrict (preserve previous behavior).
|
||||
|
||||
const Point c = Point::new_scale(center.x(), center.y());
|
||||
const double r_scaled = scale_(radius);
|
||||
const double r2 = r_scaled * r_scaled;
|
||||
|
||||
// The spiral traces a full circle of `radius` around `center`, so the center must lie
|
||||
// inside the printable area and every edge must be at least `radius` away from it.
|
||||
if (!area->contains(c))
|
||||
return false;
|
||||
const Points &pts = area->points;
|
||||
for (size_t i = 0, n = pts.size(); i < n; ++i)
|
||||
if (Line::distance_to_squared(c, pts[i], pts[(i + 1) % n]) < r2)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
|
||||
@@ -731,14 +812,19 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
|
||||
}
|
||||
|
||||
// BBS: spiral lift only safe with known position
|
||||
// TODO: check the arc will move within bed area
|
||||
if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
|
||||
double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
|
||||
// static spiral alignment when no move in x,y plane.
|
||||
// spiral centra is a radius distance to the right (y=0)
|
||||
// spiral centra is a radius distance to the right (y=0)
|
||||
Vec2d ij_offset = { radius, 0 };
|
||||
if (target_lift > 0) {
|
||||
// Orca: keep the spiral inside the active extruder's printable area, otherwise
|
||||
// fall back to a normal lift to avoid colliding with the print boundary. m_pos
|
||||
// includes the plate offset, so remove it to match the printable area coordinates.
|
||||
const Vec2d spiral_center = { m_pos.x() - m_x_offset + ij_offset.x(), m_pos.y() - m_y_offset + ij_offset.y() };
|
||||
if (target_lift > 0 && this->spiral_lift_fits_printable_area(spiral_center, radius)) {
|
||||
lift_move = this->_spiral_travel_to_z(m_pos(2) + target_lift, ij_offset, "spiral lift Z");
|
||||
} else if (target_lift > 0) {
|
||||
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
|
||||
}
|
||||
}
|
||||
//BBS: if position is unknown use normal lift
|
||||
@@ -793,7 +879,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
double radius = delta(2) / (2 * PI * atan(this->filament()->travel_slope()));
|
||||
Vec2d ij_offset = radius * delta_no_z.normalized();
|
||||
ij_offset = { -ij_offset(1), ij_offset(0) };
|
||||
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
|
||||
// Orca: only perform the spiral lift if its full circle stays inside the
|
||||
// printable area of the active extruder, otherwise fall back to a normal
|
||||
// lift to avoid colliding with the print boundary. `source` is already in
|
||||
// bed coordinates (plate offset removed), matching the printable area.
|
||||
const Vec2d spiral_center = { source.x() + ij_offset.x(), source.y() + ij_offset.y() };
|
||||
if (this->spiral_lift_fits_printable_area(spiral_center, radius))
|
||||
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
|
||||
else
|
||||
slop_move = _travel_to_z(target.z(), "normal lift Z");
|
||||
}
|
||||
//BBS: SlopeLift
|
||||
else if (m_to_lift_type == LiftType::SlopeLift &&
|
||||
@@ -925,45 +1019,48 @@ std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, c
|
||||
}
|
||||
|
||||
if (!this->config.enable_arc_fitting) { // Orca: if arc fitting is disabled, approximate the arc with small linear segments
|
||||
std::ostringstream oss;
|
||||
const double z_start = m_pos(2); // starting Z height
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// Determine number of segments based on Resolution
|
||||
// --------------------------------------------------------------------
|
||||
const double ref_resolution = 0.01; // reference resolution in mm
|
||||
const double ref_segments = 8.0; // reference number of segments at reference resolution
|
||||
|
||||
// number of linear segments to use for approximating the arc, clamp between 4 and 16
|
||||
const int segments = std::clamp(int(std::round(ref_segments * (ref_resolution / m_resolution))), 4, 16);
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
const double px = m_pos(0) - m_x_offset; // take plate offset into consideration
|
||||
const double py = m_pos(1) - m_y_offset; // take plate offset into consideration
|
||||
const double cx = px + ij_offset(0); // center x
|
||||
const double cy = py + ij_offset(1); // center y
|
||||
const double radius = ij_offset.norm(); // radius
|
||||
|
||||
// Number of linear segments approximating the circle, chosen so that a chord never deviates
|
||||
// from the true arc by more than the slicing resolution. A resolution of 0 means "no
|
||||
// simplification", which has no finite segment count, so it takes the upper bound.
|
||||
constexpr size_t min_segments = 8; // keep a small spiral visibly round
|
||||
constexpr size_t max_segments = 128; // bound the emitted G-code
|
||||
const int segments = int(m_resolution > 0. ?
|
||||
std::clamp(Geometry::ArcWelder::arc_discretization_steps(radius, 2. * M_PI, m_resolution), min_segments, max_segments) :
|
||||
max_segments);
|
||||
|
||||
const double a0 = std::atan2(py - cy, px - cx); // start angle
|
||||
const double delta = 2.0 * M_PI; // CCW full circle
|
||||
|
||||
if (full_gcode_comment)
|
||||
oss << ";" << comment << "\n";
|
||||
auto emit_point = [&output](const Vec3d &point) {
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xyz(point);
|
||||
output += w.string();
|
||||
};
|
||||
|
||||
oss << "G1 F" << (speed * 60.0) << "\n"; // set feedrate
|
||||
output.reserve(size_t(segments) * 40); // ~40 characters per emitted G1 line
|
||||
|
||||
GCodeG1Formatter w; // set feedrate
|
||||
w.emit_f(speed * 60.0);
|
||||
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
output += w.string();
|
||||
|
||||
// approximate the arc with small linear segments (without the last point which is added later to ensure exactness)
|
||||
for (int i = 1; i < segments; ++i) {
|
||||
double t = double(i) / segments; // parametric position along arc
|
||||
double a = a0 + delta * t; // CCW arc param
|
||||
double x = cx + radius * std::cos(a); // point on circle
|
||||
double y = cy + radius * std::sin(a); // point on circle
|
||||
double zz = z_start + (z - z_start) * t; // interpolated Z height
|
||||
|
||||
oss << "G1 X" << x << " Y" << y << " Z" << zz << "\n";
|
||||
const double t = double(i) / segments; // parametric position along arc
|
||||
const double a = a0 + 2. * M_PI * t; // CCW arc param, full circle
|
||||
emit_point(Vec3d(cx + radius * std::cos(a), // point on circle
|
||||
cy + radius * std::sin(a),
|
||||
z_start + (z - z_start) * t)); // interpolated Z height
|
||||
}
|
||||
|
||||
oss << "G1 X" << px << " Y" << py << " Z" << z << "\n"; // final point to ensure exactness
|
||||
output = oss.str();
|
||||
emit_point(Vec3d(px, py, z)); // final point to ensure exactness
|
||||
} else { // Orca: if arc fitting is enabled emit a G2/G3 command for the spiral lift
|
||||
output = std::string("G17") + (full_gcode_comment ? " ; XY plane for arc\n" : "\n");
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <charconv>
|
||||
#include "Extruder.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "GCode/CoolingBuffer.hpp"
|
||||
|
||||
@@ -132,6 +133,8 @@ public:
|
||||
const bool is_bbl_printers() const {return m_is_bbl_printers;}
|
||||
void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
|
||||
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
|
||||
void invalidate_acceleration() { m_last_acceleration = 0; m_last_travel_acceleration = 0; }
|
||||
void invalidate_jerk() { m_last_jerk = 0; }
|
||||
|
||||
// Returns whether this flavor supports separate print and travel acceleration.
|
||||
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
|
||||
@@ -181,6 +184,14 @@ public:
|
||||
|
||||
// Orca: slicing resolution in mm
|
||||
double m_resolution = 0.01;
|
||||
// Orca: printable area polygons (scaled, bed coordinates) used to keep spiral lifts
|
||||
// from colliding with the print boundary. m_extruder_printable_areas holds the
|
||||
// per-extruder reachable area (intersected with the bed) when a printer defines
|
||||
// different boundaries per extruder; m_bed_printable_area is the global fallback.
|
||||
// Storing full polygons (rather than a bounding box) keeps the check correct for
|
||||
// non-rectangular beds such as delta/circular printers.
|
||||
Polygon m_bed_printable_area;
|
||||
std::vector<Polygon> m_extruder_printable_areas;
|
||||
|
||||
std::string m_gcode_label_objects_start;
|
||||
std::string m_gcode_label_objects_end;
|
||||
@@ -197,6 +208,10 @@ public:
|
||||
|
||||
std::string _travel_to_z(double z, const std::string &comment);
|
||||
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
|
||||
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
|
||||
const Polygon *active_printable_area() const;
|
||||
// Orca: true if a full spiral-lift circle (center in bed coordinates, mm) fits inside the active printable area.
|
||||
bool spiral_lift_fits_printable_area(const Vec2d ¢er, double radius) const;
|
||||
std::string _retract(double length, double restart_extra, const std::string &comment);
|
||||
std::string set_acceleration_internal(Acceleration type, unsigned int acceleration);
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
|
||||
void* volume{nullptr};
|
||||
std::vector<int>* plane_indices{nullptr};
|
||||
Transform3d world_tran;
|
||||
Transform3d world_tran = Transform3d::Identity();
|
||||
std::shared_ptr<std::vector<SurfaceFeature>> world_plane_features{nullptr};
|
||||
std::shared_ptr<SurfaceFeature> origin_surface_feature{nullptr};
|
||||
|
||||
|
||||
@@ -3248,9 +3248,9 @@ double Model::findMaxSpeed(const ModelObject* object) {
|
||||
if (objectKey == "outer_wall_speed")
|
||||
externalPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
|
||||
if (objectKey == "small_perimeter_speed")
|
||||
smallPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
|
||||
smallPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(externalPerimeterSpeedObj);
|
||||
if (objectKey == "small_support_perimeter_speed")
|
||||
smallSupportPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
|
||||
smallSupportPerimeterSpeedObj = object->config.get().opt_float_or_percent_nullable(objectKey, 0).get_abs_value(supportSpeedObj);
|
||||
}
|
||||
objMaxSpeed = std::max(perimeterSpeedObj, std::max(externalPerimeterSpeedObj, std::max(infillSpeedObj, std::max(solidInfillSpeedObj, std::max(topSolidInfillSpeedObj, std::max(supportSpeedObj, std::max(smallPerimeterSpeedObj, std::max(smallSupportPerimeterSpeedObj, objMaxSpeed))))))));
|
||||
if (objMaxSpeed <= 0) objMaxSpeed = 250.;
|
||||
|
||||
@@ -1177,6 +1177,18 @@ static bool is_volume_sinking(const indexed_triangle_set &its, const Transform3d
|
||||
|
||||
//#define MMU_SEGMENTATION_DEBUG_TOP_BOTTOM
|
||||
|
||||
double resolve_outer_wall_line_width(const PrintRegionConfig ®ion_config, const PrintObjectConfig &object_config, const PrintConfig &print_config)
|
||||
{
|
||||
// A filament id of 0 underflows, and get_at() then falls back to the first nozzle.
|
||||
const double nozzle_diameter = print_config.nozzle_diameter.get_at(region_config.outer_wall_filament_id - 1);
|
||||
ConfigOptionFloatOrPercent width = region_config.outer_wall_line_width;
|
||||
if (width.value == 0)
|
||||
width = object_config.line_width;
|
||||
if (!width.percent && width.value <= 0.)
|
||||
return Flow::auto_extrusion_width(frExternalPerimeter, float(nozzle_diameter));
|
||||
return width.get_abs_value(nozzle_diameter);
|
||||
}
|
||||
|
||||
// Returns segmentation of top and bottom layers based on painting in segmentation gizmos.
|
||||
static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_layers(const PrintObject &print_object,
|
||||
const std::vector<ExPolygons> &input_expolygons,
|
||||
@@ -1347,8 +1359,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
|
||||
// As this region may split existing regions, we collect statistics over all regions for color_idx == 0.
|
||||
color_idx == 0 || config.outer_wall_filament_id == int(color_idx)) {
|
||||
//BBS: the extrusion line width is outer wall rather than inner wall
|
||||
const double nozzle_diameter = print_object.print()->config().nozzle_diameter.get_at(0);
|
||||
double outer_wall_line_width = config.get_abs_value("outer_wall_line_width", nozzle_diameter);
|
||||
double outer_wall_line_width = resolve_outer_wall_line_width(config, print_object.config(), print_object.print()->config());
|
||||
out.extrusion_width = std::max<float>(out.extrusion_width, outer_wall_line_width);
|
||||
out.top_shell_layers = std::max<int>(out.top_shell_layers, config.top_shell_layers);
|
||||
out.bottom_shell_layers = std::max<int>(out.bottom_shell_layers, config.bottom_shell_layers);
|
||||
|
||||
@@ -9,6 +9,9 @@ namespace Slic3r {
|
||||
class ExPolygon;
|
||||
class ModelVolume;
|
||||
class PrintObject;
|
||||
class PrintConfig;
|
||||
class PrintObjectConfig;
|
||||
class PrintRegionConfig;
|
||||
class FacetsAnnotation;
|
||||
|
||||
using ExPolygons = std::vector<ExPolygon>;
|
||||
@@ -52,6 +55,9 @@ std::vector<std::vector<ExPolygons>> multi_material_segmentation_by_painting(con
|
||||
// Returns fuzzy skin segmentation based on painting in fuzzy skin segmentation gizmo
|
||||
std::vector<std::vector<ExPolygons>> fuzzy_skin_segmentation_by_painting(const PrintObject &print_object, const std::function<void()> &throw_on_cancel_callback);
|
||||
|
||||
// Effective outer-wall line width for a region, resolved against its own nozzle with PrintRegion::flow's fallback.
|
||||
double resolve_outer_wall_line_width(const PrintRegionConfig ®ion_config, const PrintObjectConfig &object_config, const PrintConfig &print_config);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
namespace boost::polygon {
|
||||
|
||||
+35
-85
@@ -48,100 +48,50 @@ struct OrientMesh {
|
||||
|
||||
};
|
||||
|
||||
// params for minimizing support area
|
||||
struct OrientParamsArea {
|
||||
float TAR_A = 0.015f;
|
||||
float TAR_B = 0.177f;
|
||||
float RELATIVE_F = 20;
|
||||
float CONTOUR_F = 0.5f;
|
||||
float BOTTOM_F = 2.5f;
|
||||
float BOTTOM_HULL_F = 0.1f;
|
||||
float TAR_C = 0.1f;
|
||||
float TAR_D = 1;
|
||||
float TAR_E = 0.0115f;
|
||||
float FIRST_LAY_H = 0.2f;//0.0475;
|
||||
float VECTOR_TOL = -0.00083f;
|
||||
float NEGL_FACE_SIZE = 0.01f;
|
||||
float ASCENT = -0.5f;
|
||||
float PLAFOND_ADV = 0.0599f;
|
||||
float CONTOUR_AMOUNT = 0.0182427f;
|
||||
float OV_H = 2.574f;
|
||||
float height_offset = 2.3728f;
|
||||
float height_log = 0.041375f;
|
||||
float height_log_k = 1.9325457f;
|
||||
float LAF_MAX = 0.999f; // cos(1.4\degree) for low angle face 0.9997f
|
||||
float LAF_MIN = 0.97f; // cos(14\degree) 0.9703f
|
||||
float TAR_LAF = 0.001f; //0.01f
|
||||
float TAR_PROJ_AREA = 0.1f;
|
||||
float BOTTOM_MIN = 0.1f; // min bottom area. If lower than it the object may be unstable
|
||||
float BOTTOM_MAX = 2000; // max bottom area. If get to it the object is stable enough (further increase bottom area won't do more help)
|
||||
float height_to_bottom_hull_ratio_MIN = 1;
|
||||
float BOTTOM_HULL_MAX = 2000;// max bottom hull area
|
||||
float APPERANCE_FACE_SUPP=3; // penalty of generating supports on appearance face
|
||||
|
||||
float overhang_angle = 60.f;
|
||||
bool use_low_angle_face = true;
|
||||
bool min_volume = false;
|
||||
Eigen::Vector3f fun_dir;
|
||||
|
||||
/// Allow parallel execution.
|
||||
bool parallel = true;
|
||||
|
||||
/// Progress indicator callback called when an object gets packed.
|
||||
/// The unsigned argument is the number of items remaining to pack.
|
||||
std::function<void(unsigned, std::string)> progressind = {};
|
||||
|
||||
/// A predicate returning true if abort is needed.
|
||||
std::function<bool(void)> stopcondition = {};
|
||||
|
||||
OrientParamsArea() = default;
|
||||
};
|
||||
|
||||
struct OrientParams {
|
||||
float TAR_A = 0.01f;//0.128f;
|
||||
float TAR_B = 0.177f;
|
||||
float RELATIVE_F= 6.610621027964314f;
|
||||
float CONTOUR_F = 0.23228623269775997f;
|
||||
float BOTTOM_F = 1.167152017941474f;
|
||||
float BOTTOM_HULL_F = 0.1f;
|
||||
float TAR_C = 0.24308070476924726f;
|
||||
float TAR_D = 0.6284515508160871f;
|
||||
float TAR_E = 0;//0.032157292647062234;
|
||||
float FIRST_LAY_H = 0.2f;//0.029;
|
||||
float VECTOR_TOL = -0.0011163303070972383f;
|
||||
float NEGL_FACE_SIZE = 0.1f;
|
||||
float ASCENT= -0.5f;
|
||||
float PLAFOND_ADV = 0.04079208948120519f;
|
||||
float CONTOUR_AMOUNT = 0.0101472219892684f;
|
||||
float OV_H = 1.0370178217794535f;
|
||||
float height_offset = 2.7417608343142073f;
|
||||
float height_log = 0.06442030687034085f;
|
||||
float height_log_k = 0.3933594673063997f;
|
||||
float LAF_MAX = 0.999f; // cos(1.4\degree) for low angle face //0.9997f;
|
||||
float LAF_MIN= 0.9703f; // cos(14\degree) 0.9703f;
|
||||
float TAR_LAF = 0.01f; //0.1f
|
||||
float TAR_PROJ_AREA = 0.1f;
|
||||
float BOTTOM_MIN = 0.1f; // min bottom area. If lower than it the objects may be unstable
|
||||
float BOTTOM_MAX = 2000; //400
|
||||
float height_to_bottom_hull_ratio_MIN = 1;
|
||||
float BOTTOM_HULL_MAX = 2000;// max bottom hull area to clip //600
|
||||
float APPERANCE_FACE_SUPP=3; // penalty of generating supports on appearance face
|
||||
|
||||
float overhang_angle = 60.f;
|
||||
bool use_low_angle_face = true;
|
||||
bool min_volume = false;
|
||||
Eigen::Vector3f fun_dir;
|
||||
float TAR_A { 0.01f }; // 0.128f;
|
||||
float TAR_B { 0.177f };
|
||||
float RELATIVE_F { 6.610621027964314f };
|
||||
float CONTOUR_F { 0.23228623269775997f };
|
||||
float BOTTOM_F { 1.167152017941474f };
|
||||
float BOTTOM_HULL_F { 0.1f };
|
||||
float TAR_C { 0.24308070476924726f };
|
||||
float TAR_D { 0.6284515508160871f };
|
||||
float TAR_E { 0}; // 0.032157292647062234;
|
||||
float FIRST_LAY_H { 0.2f}; // 0.029;
|
||||
float VECTOR_TOL { -0.0011163303070972383f };
|
||||
float NEGL_FACE_SIZE { 0.1f };
|
||||
float ASCENT { -0.5f };
|
||||
float PLAFOND_ADV { 0.04079208948120519f };
|
||||
float CONTOUR_AMOUNT { 0.0101472219892684f };
|
||||
float OV_H { 1.0370178217794535f };
|
||||
float height_offset { 2.7417608343142073f };
|
||||
float height_log { 0.06442030687034085f };
|
||||
float height_log_k { 0.3933594673063997f };
|
||||
float LAF_MAX { 0.999f }; // cos(1.4\degree) for low angle face //0.9997f;
|
||||
float LAF_MIN { 0.9703f }; // cos(14\degree) 0.9703f;
|
||||
float TAR_LAF { 0.01f }; // 0.1f
|
||||
float TAR_PROJ_AREA { 0.1f };
|
||||
float BOTTOM_MIN { 0.1f }; // min bottom area. If lower than it the objects may be unstable
|
||||
float BOTTOM_MAX { 2000 }; // 400
|
||||
float height_to_bottom_hull_ratio_MIN { 1 };
|
||||
float BOTTOM_HULL_MAX { 2000 }; // max bottom hull area to clip //600
|
||||
float APPERANCE_FACE_SUPP { 3 }; // penalty of generating supports on appearance face
|
||||
|
||||
float overhang_angle { 60.f };
|
||||
bool use_low_angle_face { true };
|
||||
bool min_volume { false };
|
||||
Eigen::Vector3f fun_dir {};
|
||||
|
||||
/// Allow parallel execution.
|
||||
bool parallel = false;
|
||||
bool parallel { false };
|
||||
|
||||
/// Progress indicator callback called when an object gets packed.
|
||||
/// The unsigned argument is the number of items remaining to pack.
|
||||
std::function<void(unsigned, std::string)> progressind = {};
|
||||
std::function<void(unsigned, std::string)> progressind {};
|
||||
|
||||
/// A predicate returning true if abort is needed.
|
||||
std::function<bool(void)> stopcondition = {};
|
||||
std::function<bool(void)> stopcondition {};
|
||||
|
||||
OrientParams() = default;
|
||||
};
|
||||
|
||||
@@ -360,6 +360,14 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con
|
||||
return clipped_paths;
|
||||
}
|
||||
|
||||
static double clipper_z_path_length(const ClipperLib_Z::Path &path)
|
||||
{
|
||||
double len = 0.;
|
||||
for (size_t i = 1; i < path.size(); ++ i)
|
||||
len += (Vec2d(double(path[i].x()), double(path[i].y())) - Vec2d(double(path[i - 1].x()), double(path[i - 1].y()))).norm();
|
||||
return len;
|
||||
}
|
||||
|
||||
struct PerimeterGeneratorArachneExtrusion
|
||||
{
|
||||
Arachne::ExtrusionLine* extrusion = nullptr;
|
||||
@@ -571,17 +579,156 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
|
||||
return extrusion_coll;
|
||||
}
|
||||
|
||||
// ORCA: only_one_wall_top detects the top as "slice − upper", so a feature rising from the middle of a
|
||||
// top surface becomes an enclosed hole that gets ringed with extra inner walls. Fill those holes back
|
||||
// into the top. Only holes that are both covered by the upper layer (excludes bridges) and backed by
|
||||
// solid material (excludes voids) are filled.
|
||||
static ExPolygons fill_enclosed_top_feature_holes(const ExPolygons &top, const Polygons &covered_by_upper, const ExPolygons &solid)
|
||||
// ORCA: only_one_wall_top acts on top surfaces, so without a top shell there is nothing for it to act on: zero top
|
||||
// shell layers retype the top surfaces as internal, see LayerRegion::prepare_fill_surfaces(). A 0% top surface
|
||||
// density does leave a top surface - just an unfilled one - so it does not disable the feature.
|
||||
// ConfigManipulation::toggle_print_fff_options() hides the option under the same condition, so a profile that left
|
||||
// it enabled does not act behind a hidden checkbox.
|
||||
static bool has_top_shell_layers(const PrintRegionConfig &config)
|
||||
{
|
||||
ExPolygons filled = top;
|
||||
for (ExPolygon &ex : filled)
|
||||
ex.holes.clear();
|
||||
const ExPolygons feature_holes = intersection_ex(intersection_ex(diff_ex(filled, top), covered_by_upper), solid);
|
||||
return feature_holes.empty() ? top : union_ex(top, feature_holes);
|
||||
return config.top_shell_layers.value > 0;
|
||||
}
|
||||
|
||||
// ORCA: only_one_wall_first_layer thins the first layer to a single wall, the bottom counterpart of the above and
|
||||
// gated the same way: zero bottom shell layers retype the bottom surfaces as internal, so that wall would ring
|
||||
// sparse infill on the bed. The bottom surface density plays no part - an unfilled bottom surface is still a bottom
|
||||
// surface, exactly as for the top - and it cannot reach zero anyway, being capped at a 10% minimum.
|
||||
static bool has_bottom_shell_layers(const PrintRegionConfig &config)
|
||||
{
|
||||
return config.bottom_shell_layers.value > 0;
|
||||
}
|
||||
|
||||
// ORCA: the inner walls are only given up when a top fill takes their space, and it has to actually reach it -
|
||||
// a 0% top surface density leaves no fill at all, and without top_surface_expansion the fill never grows over
|
||||
// them. Either way the original generation is kept (re-onion the not-top region), which is what users of
|
||||
// only_one_wall_top alone have always got.
|
||||
static bool top_fill_replaces_inner_walls(const PrintRegionConfig &config)
|
||||
{
|
||||
return has_top_shell_layers(config) && config.top_surface_density.value > 0 && config.top_surface_expansion.value > 0;
|
||||
}
|
||||
|
||||
// ORCA: only_one_wall_top - cheap per-vertex classification of a wall against the top surface. Only Partial
|
||||
// needs the geometry clipped or measured; a segment crossing the top with no vertex inside is rare enough to ignore.
|
||||
enum class TopOverlap { None, Partial, Full };
|
||||
|
||||
static bool point_over_top(const Point &p, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
|
||||
{
|
||||
if (! top_region_bbox.contains(p))
|
||||
return false;
|
||||
for (const ExPolygon &ex : top_region)
|
||||
if (ex.contains(p, false))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static TopOverlap classify_over_top(const Points &pts, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
|
||||
{
|
||||
size_t inside = 0;
|
||||
for (const Point &p : pts)
|
||||
if (point_over_top(p, top_region, top_region_bbox))
|
||||
++ inside;
|
||||
return inside == 0 ? TopOverlap::None : inside == pts.size() ? TopOverlap::Full : TopOverlap::Partial;
|
||||
}
|
||||
|
||||
static TopOverlap classify_over_top(const Arachne::ExtrusionLine &el, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
|
||||
{
|
||||
size_t inside = 0;
|
||||
for (const Arachne::ExtrusionJunction &j : el.junctions)
|
||||
if (point_over_top(j.p, top_region, top_region_bbox))
|
||||
++ inside;
|
||||
return inside == 0 ? TopOverlap::None : inside == el.junctions.size() ? TopOverlap::Full : TopOverlap::Partial;
|
||||
}
|
||||
|
||||
// ORCA: only_one_wall_top for Arachne - cut out of the already generated inner walls the parts running over the top
|
||||
// surface, so geometry that continues upward keeps its walls. A wall too short over the top to be worth slitting open
|
||||
// is left whole, its footprint reported in kept_over_top for the caller to withhold from the top fill.
|
||||
static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &inner_perimeters, const ExPolygons &top_region, coord_t perimeter_width, Polygons &kept_over_top)
|
||||
{
|
||||
const BoundingBox top_region_bbox = get_extents(top_region).inflated(SCALED_EPSILON);
|
||||
auto covered_by = [](const Arachne::ExtrusionLine &el) {
|
||||
Polyline centerline;
|
||||
centerline.points.reserve(el.junctions.size());
|
||||
coord_t width = 0;
|
||||
for (const Arachne::ExtrusionJunction &j : el.junctions) {
|
||||
centerline.points.emplace_back(j.p);
|
||||
width = std::max(width, j.w);
|
||||
}
|
||||
return offset(centerline, float(width) / 2.f);
|
||||
};
|
||||
// Pull the cut back by half a wall width: the clip severs the centerline, but the bead's rounded end
|
||||
// extends half a width past its endpoint and would otherwise overlap the top fill.
|
||||
ClipperLib_Z::Paths top_paths_z;
|
||||
for (const Polygon &poly : to_polygons(offset_ex(top_region, float(perimeter_width) / 2.f))) {
|
||||
top_paths_z.emplace_back();
|
||||
ClipperLib_Z::Path &out = top_paths_z.back();
|
||||
out.reserve(poly.points.size());
|
||||
for (const Point &pt : poly.points)
|
||||
out.emplace_back(pt.x(), pt.y(), 0);
|
||||
}
|
||||
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
|
||||
Arachne::VariableWidthLines kept;
|
||||
kept.reserve(inner_perimeter.size());
|
||||
for (Arachne::ExtrusionLine &el : inner_perimeter) {
|
||||
if (el.empty())
|
||||
continue;
|
||||
const TopOverlap overlap = classify_over_top(el, top_region, top_region_bbox);
|
||||
if (overlap == TopOverlap::None) {
|
||||
kept.emplace_back(std::move(el));
|
||||
continue;
|
||||
}
|
||||
if (overlap == TopOverlap::Full)
|
||||
continue; // the clip below would return nothing anyway
|
||||
ClipperLib_Z::Path subject;
|
||||
subject.reserve(el.size());
|
||||
for (const Arachne::ExtrusionJunction &j : el.junctions)
|
||||
subject.emplace_back(j.p.x(), j.p.y(), j.w);
|
||||
ClipperLib_Z::Paths pieces = clip_extrusion(subject, top_paths_z, ClipperLib_Z::ctDifference);
|
||||
|
||||
// Clipper treats the subject as an open polyline, so it also cuts a closed loop at its (arbitrary)
|
||||
// start vertex and may reverse pieces. Stitch pieces sharing an endpoint back together.
|
||||
auto same_pt = [](const ClipperLib_Z::IntPoint &p, const ClipperLib_Z::IntPoint &q) {
|
||||
return std::abs(p.x() - q.x()) <= SCALED_EPSILON && std::abs(p.y() - q.y()) <= SCALED_EPSILON;
|
||||
};
|
||||
for (size_t i = 0; i < pieces.size(); ++ i) {
|
||||
for (size_t j = i + 1; j < pieces.size();) {
|
||||
ClipperLib_Z::Path &a = pieces[i];
|
||||
ClipperLib_Z::Path &b = pieces[j];
|
||||
if (same_pt(a.front(), b.front()) || same_pt(a.front(), b.back()))
|
||||
std::reverse(a.begin(), a.end());
|
||||
if (same_pt(a.back(), b.back()))
|
||||
std::reverse(b.begin(), b.end());
|
||||
if (same_pt(a.back(), b.front())) {
|
||||
a.insert(a.end(), b.begin() + 1, b.end());
|
||||
pieces.erase(pieces.begin() + j);
|
||||
j = i + 1; // the merged path has new endpoints, restart the scan
|
||||
} else
|
||||
++ j;
|
||||
}
|
||||
}
|
||||
|
||||
// If the clip removed next to nothing, keep the loop untouched instead of slitting it open. The
|
||||
// half-width pull-back above already costs about one width per crossing, hence two widths.
|
||||
double kept_length = 0.;
|
||||
for (const ClipperLib_Z::Path &path : pieces)
|
||||
kept_length += clipper_z_path_length(path);
|
||||
if (clipper_z_path_length(subject) - kept_length < 2. * double(perimeter_width)) {
|
||||
append(kept_over_top, covered_by(el));
|
||||
kept.emplace_back(std::move(el));
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const ClipperLib_Z::Path &path : pieces) {
|
||||
Arachne::ExtrusionLine clipped(el.inset_idx, el.is_odd);
|
||||
clipped.junctions.reserve(path.size());
|
||||
for (const ClipperLib_Z::IntPoint &pt : path)
|
||||
clipped.junctions.emplace_back(Point(pt.x(), pt.y()), coord_t(pt.z()), el.inset_idx);
|
||||
// Discard tiny leftovers that would print as zits.
|
||||
if (clipped.size() >= 2 && clipped.getLength() >= perimeter_width)
|
||||
kept.emplace_back(std::move(clipped));
|
||||
}
|
||||
}
|
||||
inner_perimeter = std::move(kept);
|
||||
}
|
||||
}
|
||||
|
||||
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
|
||||
@@ -649,7 +796,6 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP
|
||||
ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes);
|
||||
|
||||
ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes);
|
||||
top_polygons = fill_enclosed_top_feature_holes(top_polygons, upper_polygons_series_clipped, orig_polygons);
|
||||
|
||||
// get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the
|
||||
// min_width_top_surface we removed a bit before)
|
||||
@@ -1234,6 +1380,11 @@ void PerimeterGenerator::process_classic()
|
||||
for (const Surface &surface : all_surfaces)
|
||||
surface_exp.push_back(surface.expolygon);
|
||||
std::vector<size_t> surface_order = chain_expolygons(surface_exp);
|
||||
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
|
||||
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
|
||||
// topmost and first layers - sees the same answer.
|
||||
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
|
||||
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
|
||||
for (size_t order_idx = 0; order_idx < surface_order.size(); order_idx++) {
|
||||
const Surface &surface = all_surfaces[surface_order[order_idx]];
|
||||
// detect how many perimeters must be generated for this island
|
||||
@@ -1241,16 +1392,23 @@ void PerimeterGenerator::process_classic()
|
||||
int sparse_infill_density = this->config->sparse_infill_density.value;
|
||||
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
|
||||
loop_number++;
|
||||
if (this->layer_id == object_config->raft_layers && this->config->only_one_wall_first_layer)
|
||||
if (this->layer_id == object_config->raft_layers && only_one_wall_first_layer)
|
||||
loop_number = 0;
|
||||
// Set the topmost layer to be one wall
|
||||
if (loop_number > 0 && config->only_one_wall_top && this->upper_slices == nullptr)
|
||||
if (loop_number > 0 && only_one_wall_top && this->upper_slices == nullptr)
|
||||
loop_number = 0;
|
||||
|
||||
ExPolygons last = union_ex(surface.expolygon.simplify_p(surface_simplify_resolution));
|
||||
ExPolygons gaps;
|
||||
ExPolygons top_fills;
|
||||
ExPolygons fill_clip;
|
||||
// ORCA: only_one_wall_top, all empty unless this island has a top surface on this layer. See the
|
||||
// post-onion reduction below: the region to keep clear of inner walls, the space freed by the dropped
|
||||
// walls (goes to infill, not left as a void) and the space held by the kept ones (withheld from the fill).
|
||||
ExPolygons one_wall_top_region;
|
||||
ExPolygons one_wall_top_reclaimed;
|
||||
Polygons one_wall_top_kept_bands;
|
||||
bool apply_one_wall_top = false;
|
||||
if (loop_number >= 0) {
|
||||
// In case no perimeters are to be generated, loop_number will equal to -1.
|
||||
std::vector<PerimeterGeneratorLoops> contours(loop_number+1); // depth => loops
|
||||
@@ -1389,8 +1547,19 @@ void PerimeterGenerator::process_classic()
|
||||
|
||||
//BBS: refer to superslicer
|
||||
//store surface for top infill if only_one_wall_top
|
||||
if (i == 0 && i!=loop_number && config->only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
|
||||
this->split_top_surfaces(last, top_fills, last, fill_clip);
|
||||
if (i == 0 && i!=loop_number && only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
|
||||
if (top_fill_replaces_inner_walls(*this->config)) {
|
||||
// ORCA: take the top fill and the keep-out region but leave `last` as the real geometry,
|
||||
// so the onion follows it and the walls over the top are reduced in one step below.
|
||||
ExPolygons non_top_polygons;
|
||||
this->split_top_surfaces(last, top_fills, non_top_polygons, fill_clip);
|
||||
apply_one_wall_top = !top_fills.empty();
|
||||
if (apply_one_wall_top)
|
||||
one_wall_top_region = diff_ex(last, non_top_polygons);
|
||||
} else {
|
||||
// Onion the not-top region only, so the remaining walls stop at the top boundary.
|
||||
this->split_top_surfaces(last, top_fills, last, fill_clip);
|
||||
}
|
||||
}
|
||||
|
||||
if (i == loop_number && (! has_gap_fill || this->config->sparse_infill_density.value == 0)) {
|
||||
@@ -1400,6 +1569,46 @@ void PerimeterGenerator::process_classic()
|
||||
}
|
||||
}
|
||||
|
||||
// ORCA: only_one_wall_top reduction - drop the inner walls (depth > 0) running over the top surface and
|
||||
// take that space back from the gaps, leaving the top with the outer wall and the top infill. Classic
|
||||
// perimeters are closed loops, so a wall can only be kept or dropped whole; one that merely grazes the
|
||||
// top (same tolerance as the Arachne clip) is kept and withheld from the top fill instead.
|
||||
if (apply_one_wall_top) {
|
||||
const BoundingBox top_region_bbox = get_extents(one_wall_top_region).inflated(SCALED_EPSILON);
|
||||
const double grazing_tolerance = 2. * double(perimeter_width);
|
||||
// The band a wall covers, taken around its centerline so the orientation of holes does not matter.
|
||||
auto wall_band = [perimeter_spacing](const Polygon &poly) {
|
||||
Polygon centerline = poly;
|
||||
centerline.make_counter_clockwise();
|
||||
return diff(offset(centerline, float(perimeter_spacing) / 2.f),
|
||||
offset(centerline, -float(perimeter_spacing) / 2.f));
|
||||
};
|
||||
Polygons dropped_wall_bands;
|
||||
auto reduce_over_top = [&](PerimeterGeneratorLoops &loops) {
|
||||
loops.erase(std::remove_if(loops.begin(), loops.end(), [&](const PerimeterGeneratorLoop &loop) {
|
||||
const TopOverlap overlap = classify_over_top(loop.polygon.points, one_wall_top_region, top_region_bbox);
|
||||
if (overlap == TopOverlap::None)
|
||||
return false;
|
||||
// Only a wall straddling the boundary is worth measuring; a wall wholly over the top goes.
|
||||
if (overlap == TopOverlap::Partial &&
|
||||
total_length(intersection_pl(Polylines{ loop.polygon.split_at_first_point() }, one_wall_top_region)) < grazing_tolerance) {
|
||||
append(one_wall_top_kept_bands, wall_band(loop.polygon));
|
||||
return false;
|
||||
}
|
||||
append(dropped_wall_bands, wall_band(loop.polygon));
|
||||
return true;
|
||||
}), loops.end());
|
||||
};
|
||||
for (int d = 1; d <= loop_number; ++ d) {
|
||||
reduce_over_top(contours[d]);
|
||||
reduce_over_top(holes[d]);
|
||||
}
|
||||
if (! gaps.empty())
|
||||
gaps = diff_ex(gaps, one_wall_top_region);
|
||||
if (! dropped_wall_bands.empty())
|
||||
one_wall_top_reclaimed = diff_ex(dropped_wall_bands, one_wall_top_region);
|
||||
}
|
||||
|
||||
// nest loops: holes first
|
||||
for (int d = 0; d <= loop_number; ++ d) {
|
||||
PerimeterGeneratorLoops &holes_d = holes[d];
|
||||
@@ -1634,7 +1843,10 @@ void PerimeterGenerator::process_classic()
|
||||
and use zigzag). */
|
||||
//FIXME Vojtech: This grows by a rounded extrusion width, not by line spacing,
|
||||
// therefore it may cover the area, but no the volume.
|
||||
last = diff_ex(last, gap_fill.polygons_covered_by_width(10.f));
|
||||
Polygons gap_fill_covered = gap_fill.polygons_covered_by_width(10.f);
|
||||
last = diff_ex(last, gap_fill_covered);
|
||||
if (! one_wall_top_reclaimed.empty())
|
||||
one_wall_top_reclaimed = diff_ex(one_wall_top_reclaimed, gap_fill_covered);
|
||||
this->gap_fill->append(std::move(gap_fill.entities));
|
||||
|
||||
}
|
||||
@@ -1679,9 +1891,15 @@ void PerimeterGenerator::process_classic()
|
||||
// append infill areas to fill_surfaces
|
||||
//if any top_fills, grow them by ext_perimeter_spacing/2 to have the real un-anchored fill
|
||||
ExPolygons top_infill_exp = intersection_ex(fill_clip, offset_ex(top_fills, double(ext_perimeter_spacing / 2)));
|
||||
// ORCA: only_one_wall_top - route the top fill around the walls kept despite grazing the top.
|
||||
if (!one_wall_top_kept_bands.empty())
|
||||
top_infill_exp = diff_ex(top_infill_exp, one_wall_top_kept_bands);
|
||||
if (!top_fills.empty()) {
|
||||
infill_exp = union_ex(infill_exp, offset_ex(top_infill_exp, double(top_infill_peri_overlap)));
|
||||
}
|
||||
// ORCA: only_one_wall_top - what the top fill does not cover of the dropped walls goes to infill.
|
||||
if (!one_wall_top_reclaimed.empty())
|
||||
infill_exp = union_ex(infill_exp, one_wall_top_reclaimed);
|
||||
this->fill_surfaces->append(infill_exp, stInternal);
|
||||
|
||||
apply_extra_perimeters(infill_exp);
|
||||
@@ -1700,6 +1918,8 @@ void PerimeterGenerator::process_classic()
|
||||
double(-inset - infill_peri_overlap));
|
||||
if (!top_fills.empty())
|
||||
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_infill_exp);
|
||||
if (!one_wall_top_reclaimed.empty())
|
||||
polyWithoutOverlap = union_ex(polyWithoutOverlap, one_wall_top_reclaimed);
|
||||
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
|
||||
}
|
||||
|
||||
@@ -1757,7 +1977,7 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
|
||||
ExPolygons unsupported = diff_ex(last, *this->lower_slices, ApplySafetyOffset::Yes);
|
||||
if (!unsupported.empty()) {
|
||||
//remove small overhangs
|
||||
ExPolygons unsupported_filtered = offset2_ex(unsupported, double(-perimeter_spacing), double(perimeter_spacing));
|
||||
ExPolygons unsupported_filtered = opening_ex(unsupported, perimeter_spacing);
|
||||
|
||||
if (!unsupported_filtered.empty()) {
|
||||
//to_draw.insert(to_draw.end(), last.begin(), last.end());
|
||||
@@ -1870,35 +2090,40 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
|
||||
//TODO: add other polys as holes inside this one (-margin)
|
||||
} else { // if(this->config->counterbore_hole_bridging.value == chbBridges)
|
||||
// Orca: Partial counterbore bridging is mask-based. Preserve the supported
|
||||
// remainder (`last`) and use simplified BridgeDetector coverage to derive the
|
||||
// remainder and use simplified BridgeDetector coverage to derive the
|
||||
// bridgeable counterbore span. The span is grown from supported material,
|
||||
// shrunk back, stripped from `last`, and expanded back. It is then prevented
|
||||
// from intruding deeper into `last` than the explicit anchor overlap.
|
||||
// Finally, add the allowed anchor band from `last` then remove the
|
||||
// shrunk back, stripped from the remaining normal surface, and expanded back.
|
||||
// It is then prevented from intruding deeper into it than the explicit anchor overlap.
|
||||
// Finally, add the allowed anchor band from it then remove the
|
||||
// narrow hole-side wall contact, which must remain unbridgeable.
|
||||
|
||||
last = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes);
|
||||
const ExPolygons remaining = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes);
|
||||
|
||||
ExPolygons bridgeable_filtered;
|
||||
|
||||
for (ExPolygon& poly : bridgeable) {
|
||||
poly.simplify(perimeter_spacing, &bridgeable_filtered);
|
||||
}
|
||||
bridgeable_filtered = opening_ex(bridgeable_filtered, ext_perimeter_width);
|
||||
|
||||
// Get rid of coarseness of the resulted bridgeable area by using the original supported area as reference.
|
||||
// This is to avoid keeping tiny bridgeable areas that are far from the supported area, or protrude into it.
|
||||
bridgeable_filtered = union_ex(offset_ex(last, perimeter_spacing), bridgeable_filtered);
|
||||
// This is to avoid keeping tiny bridgeable areas that are far from the supported area, or protrude into it.
|
||||
bridgeable_filtered = union_ex(offset_ex(remaining, perimeter_spacing), bridgeable_filtered);
|
||||
bridgeable_filtered = offset_ex(bridgeable_filtered, -perimeter_spacing);
|
||||
bridgeable_filtered = diff_ex(bridgeable_filtered, last, ApplySafetyOffset::Yes);
|
||||
bridgeable_filtered = diff_ex(bridgeable_filtered, remaining, ApplySafetyOffset::Yes);
|
||||
bridgeable_filtered = opening_ex(bridgeable_filtered, perimeter_spacing); // filter noise from the diff_ex
|
||||
bridgeable_filtered = offset_ex(bridgeable_filtered, perimeter_spacing); // restore the size to the original bridgeable area
|
||||
// Safety measure: Keep the bridge mask from intruding deeper into the
|
||||
// supported anchor region (`last`) than the explicit anchor overlap.
|
||||
bridgeable_filtered = diff_ex(bridgeable_filtered, offset_ex(last, -bridge_anchor_offset));
|
||||
// supported anchor region than the explicit anchor overlap.
|
||||
bridgeable_filtered = diff_ex(bridgeable_filtered, offset_ex(remaining, -bridge_anchor_offset));
|
||||
|
||||
ExPolygons bridge_anchor_areas = intersection_ex(last, offset_ex(unsupported_filtered, bridge_anchor_offset));
|
||||
ExPolygons bridge_anchor_areas = intersection_ex(remaining, offset_ex(unsupported_filtered, bridge_anchor_offset));
|
||||
unsupported_filtered = union_ex(bridgeable_filtered, bridge_anchor_areas); // add bridge anchor
|
||||
unsupported_filtered = opening_ex(unsupported_filtered, bridge_anchor_offset); // remove anchor area from hole-side walls, it must remain unbridgeable
|
||||
|
||||
// update 'last' only if we have a valid bridgeable area, otherwise we will lose the original unsupported area
|
||||
if (!unsupported_filtered.empty())
|
||||
last = remaining;
|
||||
// TODO: Fix the case with thin outer walls around the bridge (1~2 walls) where classic wall
|
||||
// might generate two walls in a tiny space or non at all if "Detect thin walls" is not activated
|
||||
}
|
||||
@@ -2138,6 +2363,11 @@ void PerimeterGenerator::process_arachne()
|
||||
process_no_bridge(all_surfaces, perimeter_spacing, ext_perimeter_width);
|
||||
// BBS: don't simplify too much which influence arc fitting when export gcode if arc_fitting is enabled
|
||||
double surface_simplify_resolution = (print_config->enable_arc_fitting && !this->has_fuzzy_skin) ? 0.2 * m_scaled_resolution : m_scaled_resolution;
|
||||
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
|
||||
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
|
||||
// topmost and first layers - sees the same answer.
|
||||
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
|
||||
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
|
||||
// we need to process each island separately because we might have different
|
||||
// extra perimeters for each one
|
||||
for (const Surface& surface : all_surfaces) {
|
||||
@@ -2150,12 +2380,12 @@ void PerimeterGenerator::process_arachne()
|
||||
|
||||
// Set the bottommost layer to be one wall
|
||||
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
|
||||
if (is_bottom_layer && this->config->only_one_wall_first_layer)
|
||||
if (is_bottom_layer && only_one_wall_first_layer)
|
||||
loop_number = 0;
|
||||
|
||||
// Orca: set the topmost layer to be one wall according to the config
|
||||
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
|
||||
if (is_topmost_layer && loop_number > 0 && config->only_one_wall_top)
|
||||
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
|
||||
loop_number = 0;
|
||||
|
||||
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
|
||||
@@ -2175,10 +2405,10 @@ void PerimeterGenerator::process_arachne()
|
||||
//PS: One wall top surface for Arachne
|
||||
ExPolygons top_expolygons;
|
||||
// Calculate how many inner loops remain when TopSurfaces is selected.
|
||||
const int inner_loop_number = (config->only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
|
||||
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
|
||||
|
||||
// Set one perimeter when TopSurfaces is selected.
|
||||
if (config->only_one_wall_top && loop_number > 0)
|
||||
if (only_one_wall_top && loop_number > 0)
|
||||
loop_number = 0;
|
||||
|
||||
Arachne::WallToolPathsParams input_params_tmp = input_params;
|
||||
@@ -2209,7 +2439,6 @@ void PerimeterGenerator::process_arachne()
|
||||
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
|
||||
|
||||
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
|
||||
top_expolygons = fill_enclosed_top_feature_holes(top_expolygons, upper_slices_clipped, infill_contour);
|
||||
|
||||
if (!top_expolygons.empty()) {
|
||||
if (lower_slices != nullptr) {
|
||||
@@ -2230,25 +2459,33 @@ void PerimeterGenerator::process_arachne()
|
||||
// due to thin lines being generated
|
||||
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
|
||||
|
||||
// Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges).
|
||||
const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons);
|
||||
|
||||
// Get final top ExPolygons.
|
||||
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
|
||||
top_expolygons = intersection_ex(top_expolygons, infill_contour);
|
||||
|
||||
const Polygons not_top_polygons = to_polygons(offset_ex(not_top_expolygons,wall_0_inset));
|
||||
Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
|
||||
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
|
||||
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
|
||||
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
|
||||
// top-surface islands with inner walls that don't exist when the feature is disabled.
|
||||
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
|
||||
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
|
||||
: diff_ex(infill_contour, top_expolygons),
|
||||
wall_0_inset));
|
||||
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
|
||||
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
|
||||
|
||||
// Recalculate indexes of inner perimeters before merging them.
|
||||
if (!perimeters.empty()) {
|
||||
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
|
||||
if (inner_perimeter.empty())
|
||||
continue;
|
||||
if (clip_walls_over_top) {
|
||||
Polygons kept_over_top;
|
||||
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
|
||||
// Route the top fill around the walls kept despite grazing the top.
|
||||
if (! kept_over_top.empty())
|
||||
top_expolygons = diff_ex(top_expolygons, kept_over_top);
|
||||
}
|
||||
|
||||
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
|
||||
if (!perimeters.empty())
|
||||
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
|
||||
for (Arachne::ExtrusionLine &el : inner_perimeter)
|
||||
++el.inset_idx;
|
||||
}
|
||||
}
|
||||
|
||||
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
|
||||
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
|
||||
|
||||
@@ -1791,6 +1791,8 @@ namespace client
|
||||
// from UTF8 to UTF16 don't bail out.
|
||||
msg += boost::nowide::narrow(boost::nowide::widen(error_line));
|
||||
msg += '\n';
|
||||
// The error dialog (MsgDialog.cpp) renders this excerpt monospaced. It recognizes a source
|
||||
// line directly above a caret line of spaces and a single '^'.
|
||||
for (size_t i = 0; i < error_pos; ++ i)
|
||||
msg += ' ';
|
||||
msg += "^\n";
|
||||
|
||||
@@ -129,13 +129,13 @@ public:
|
||||
std::vector<PathFittingData> fitting_result;
|
||||
//BBS: simplify points by arc fitting
|
||||
void simplify_by_fitting_arc(double tolerance);
|
||||
//BBS:
|
||||
void reset_to_linear_move();
|
||||
//BBS:
|
||||
Polylines equally_spaced_lines(double distance) const;
|
||||
|
||||
private:
|
||||
void append_fitting_result_after_append_points();
|
||||
void append_fitting_result_after_append_polyline(const Polyline& src);
|
||||
void reset_to_linear_move();
|
||||
bool split_fitting_result_before_index(const size_t index, Point &new_endpoint, std::vector<PathFittingData>& data) const;
|
||||
bool split_fitting_result_after_index(const size_t index, Point &new_startpoint, std::vector<PathFittingData>& data) const;
|
||||
};
|
||||
|
||||
@@ -1037,6 +1037,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"fill_multiline",
|
||||
"gyroid_optimized",
|
||||
"sparse_infill_pattern",
|
||||
"sparse_infill_smooth_factor",
|
||||
"lateral_lattice_angle_1",
|
||||
"lateral_lattice_angle_2",
|
||||
"infill_overhang_angle",
|
||||
@@ -1090,7 +1091,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"top_surface_speed", "support_speed", "support_object_xy_distance", "support_object_first_layer_gap", "support_interface_speed",
|
||||
"bridge_speed", "internal_bridge_speed", "gap_infill_speed", "travel_speed", "travel_speed_z", "initial_layer_speed",
|
||||
"outer_wall_acceleration", "initial_layer_acceleration", "top_surface_acceleration", "default_acceleration", "skirt_type", "skirt_loops", "skirt_speed","min_skirt_length", "skirt_distance", "skirt_start_angle", "skirt_height","single_loop_draft_shield", "draft_shield",
|
||||
"brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
|
||||
"brim_width", "brim_object_gap", "brim_flow_ratio", "brim_use_efc_outline", "combine_brims", "brim_type", "brim_ears_max_angle", "brim_ears_detection_length", "brim_ears_outer_only", "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap","enforce_support_layers",
|
||||
"raft_layers", "raft_first_layer_density", "raft_first_layer_expansion", "raft_contact_distance", "raft_expansion",
|
||||
"support_base_pattern", "support_base_pattern_spacing", "support_expansion", "support_style",
|
||||
// BBS
|
||||
@@ -1203,7 +1204,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"post_process",
|
||||
"slicing_pipeline_plugin",
|
||||
"plugins",
|
||||
"plugin_config_overrides",
|
||||
"print_plugin_config_overrides",
|
||||
"process_change_extrusion_role_gcode",
|
||||
"min_length_factor",
|
||||
"wall_maximum_resolution",
|
||||
@@ -1350,6 +1351,8 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
|
||||
"filament_retraction_length",
|
||||
"filament_retraction_minimum_travel",
|
||||
"filament_retraction_speed",
|
||||
"filament_retract_length_toolchange",
|
||||
"filament_retract_restart_extra_toolchange",
|
||||
"filament_wipe",
|
||||
"filament_z_hop",
|
||||
"filament_z_hop_types",
|
||||
@@ -1376,9 +1379,9 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
|
||||
"filament_ramming_travel_time", "filament_ramming_travel_time_nc",
|
||||
"filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc",
|
||||
"filament_preheat_temperature_delta", "filament_retract_length_nc",
|
||||
"filament_change_length_nc", "filament_prime_volume_nc",
|
||||
"filament_change_length_nc", "filament_prime_volume", "filament_prime_volume_nc",
|
||||
"long_retractions_when_ec", "retraction_distances_when_ec",
|
||||
"plugin_config_overrides",
|
||||
"filament_plugin_config_overrides",
|
||||
//ams chamber
|
||||
"filament_dev_ams_drying_ams_limitations", "filament_dev_ams_drying_temperature", "filament_dev_ams_drying_time", "filament_dev_ams_drying_heat_distortion_temperature",
|
||||
"filament_dev_chamber_drying_bed_temperature", "filament_dev_chamber_drying_time",
|
||||
@@ -1403,7 +1406,7 @@ static std::vector<std::string> s_Preset_machine_limits_options {
|
||||
static std::vector<std::string> s_Preset_printer_options {
|
||||
"printer_technology",
|
||||
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "gcode_flavor",
|
||||
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
|
||||
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
|
||||
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
|
||||
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
|
||||
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod",
|
||||
@@ -1420,7 +1423,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"use_relative_e_distances", "extruder_type", "use_firmware_retraction", "printer_notes",
|
||||
"grab_length", "support_object_skip_flush", "physical_extruder_map",
|
||||
"cooling_tube_retraction",
|
||||
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower",
|
||||
"cooling_tube_length", "high_current_on_filament_swap", "parking_pos_retraction", "extra_loading_move", "wipe_tower_type", "purge_in_prime_tower", "enable_filament_ramming", "tool_change_on_wipe_tower", "wait_for_temp_on_wipe_tower",
|
||||
"z_offset",
|
||||
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
|
||||
"bed_temperature_formula", "nozzle_flush_dataset",
|
||||
@@ -1430,7 +1433,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
// Fast-purge printer flag + device/firmware-facing per-variant extruder-change
|
||||
// deretraction speed (unconsumed by the slicer; carried by H2D/A2L/X2D/P2S machine profiles).
|
||||
"support_fast_purge_mode", "deretract_speed_extruder_change",
|
||||
"plugin_config_overrides"
|
||||
"printer_plugin_config_overrides"
|
||||
};
|
||||
|
||||
static std::vector<std::string> s_Preset_sla_print_options {
|
||||
@@ -1542,6 +1545,15 @@ const std::vector<std::string>& Preset::printer_options()
|
||||
return s_opts;
|
||||
}
|
||||
|
||||
const char* Preset::plugin_overrides_key(Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case TYPE_PRINTER: return "printer_plugin_config_overrides";
|
||||
case TYPE_FILAMENT: return "filament_plugin_config_overrides";
|
||||
default: return "print_plugin_config_overrides";
|
||||
}
|
||||
}
|
||||
|
||||
PresetCollection::PresetCollection(Preset::Type type, const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &default_name) :
|
||||
m_type(type),
|
||||
m_edited_preset(type, "", false),
|
||||
@@ -3774,12 +3786,14 @@ void PresetCollection::update_library_profile_excluded_from()
|
||||
}
|
||||
|
||||
// Check all presets that has the same alias as the filament presets with empty compatible_printers in Orca Filament Library.
|
||||
// A printer specific profile supersedes the generic one, no matter whether it lives in a vendor bundle or in the
|
||||
// library itself.
|
||||
for (const Preset& preset : m_presets) {
|
||||
if (preset.vendor == nullptr || preset.vendor->name == PresetBundle::ORCA_FILAMENT_LIBRARY)
|
||||
if (preset.vendor == nullptr)
|
||||
continue;
|
||||
|
||||
const auto* compatible_printers = dynamic_cast<const ConfigOptionStrings*>(preset.config.option("compatible_printers"));
|
||||
// All profiles in concrete vendor profile shouldn't have empty compatible_printers, but here we check it for safety.
|
||||
// Profiles with empty compatible_printers are the generic ones, they never supersede anything.
|
||||
if (compatible_printers == nullptr || compatible_printers->values.empty())
|
||||
continue;
|
||||
auto itr = excluded_froms.find(preset.alias);
|
||||
|
||||
@@ -407,6 +407,11 @@ public:
|
||||
// Printer machine limits, those are contained in printer_options().
|
||||
static const std::vector<std::string>& machine_limits_options();
|
||||
|
||||
// Option key holding this preset type's plugin capability overrides. Each type has its own key so
|
||||
// the values survive the merge into a single full config; print is the fallback for the types with
|
||||
// no plugin-backed options.
|
||||
static const char* plugin_overrides_key(Type type);
|
||||
|
||||
static const std::vector<std::string>& sla_printer_options();
|
||||
static const std::vector<std::string>& sla_material_options();
|
||||
static const std::vector<std::string>& sla_print_options();
|
||||
|
||||
@@ -49,7 +49,6 @@ static std::vector<std::string> s_project_options {
|
||||
"filament_multi_colour",
|
||||
"wipe_tower_x",
|
||||
"wipe_tower_y",
|
||||
"wipe_tower_rotation_angle",
|
||||
"curr_bed_type",
|
||||
"flush_multiplier",
|
||||
// Fast-purge mode: project-level purge control, inert at Default.
|
||||
@@ -5378,7 +5377,7 @@ void PresetBundle::update_multi_material_filament_presets(size_t to_delete_filam
|
||||
f_multiplier.resize(nozzle_nums, 1.f);
|
||||
}
|
||||
|
||||
if ( (num_filaments * num_filaments) != size_t(old_matrix.size() / old_nozzle_nums) ) {
|
||||
if (old_matrix.size() != num_filaments * num_filaments * nozzle_nums) {
|
||||
// First verify if purging volumes presets for each extruder matches number of extruders
|
||||
std::vector<double>& filaments = this->project_config.option<ConfigOptionFloats>("flush_volumes_vector")->values;
|
||||
while (filaments.size() < 2* num_filaments) {
|
||||
|
||||
+130
-44
@@ -282,9 +282,18 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "wipe_tower_x"
|
||||
|| opt_key == "wipe_tower_y"
|
||||
|| opt_key == "wipe_tower_rotation_angle") {
|
||||
// The tower gcode itself is position-independent (position and rotation are applied
|
||||
// at export), except that the wait_for_temp_on_wipe_tower park bakes a bed-relative
|
||||
// side choice into it (WipeTower2::toolchange_Change) — regenerate it when the tower
|
||||
// moves. Gating on the old config is safe: both inputs of wait_for_temp_enabled
|
||||
// invalidate psWipeTower themselves when they are part of the same diff.
|
||||
if ((opt_key == "wipe_tower_x" || opt_key == "wipe_tower_y" || opt_key == "wipe_tower_rotation_angle")
|
||||
&& WipeTower2::wait_for_temp_enabled(m_config))
|
||||
steps.emplace_back(psWipeTower);
|
||||
steps.emplace_back(psSkirtBrim);
|
||||
} else if (
|
||||
opt_key == "slicing_pipeline_plugin"
|
||||
|| opt_key == "print_plugin_config_overrides"
|
||||
|| opt_key == "initial_layer_print_height"
|
||||
|| opt_key == "nozzle_diameter"
|
||||
|| opt_key == "filament_shrink"
|
||||
@@ -381,6 +390,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "wiping_volumes_extruders"
|
||||
|| opt_key == "enable_filament_ramming"
|
||||
|| opt_key == "tool_change_on_wipe_tower"
|
||||
|| opt_key == "wait_for_temp_on_wipe_tower"
|
||||
|| opt_key == "purge_in_prime_tower"
|
||||
|| opt_key == "z_offset"
|
||||
|| opt_key == "support_multi_bed_types"
|
||||
@@ -1038,13 +1048,14 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y));
|
||||
wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y + depth));
|
||||
wipe_tower_convex_hull.points.emplace_back(scale_(x), scale_(y + depth));
|
||||
wipe_tower_convex_hull.rotate(a);
|
||||
wipe_tower_convex_hull.rotate(Geometry::deg2rad(a), Point(scale_(x), scale_(y)));
|
||||
convex_hulls_temp.push_back(wipe_tower_convex_hull);
|
||||
} else {
|
||||
//here, wipe_tower_polygon is not always convex.
|
||||
Polygon wipe_tower_polygon;
|
||||
if (print.wipe_tower_data().wipe_tower_mesh_data)
|
||||
wipe_tower_polygon = print.wipe_tower_data().wipe_tower_mesh_data->bottom;
|
||||
wipe_tower_polygon.rotate(Geometry::deg2rad(a));
|
||||
wipe_tower_polygon.translate(Point(scale_(x), scale_(y)));
|
||||
convex_hulls_temp.push_back(wipe_tower_polygon);
|
||||
}
|
||||
@@ -1063,6 +1074,22 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
|
||||
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) {
|
||||
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
|
||||
}
|
||||
// Skip the containment check for towers that will never be printed (single-filament
|
||||
// prints without smooth timelapse keep the config's tower position but emit nothing).
|
||||
// Pre-generation only the body square is tested — the auto-brim estimate can overshoot
|
||||
// the generated brim by several mm and must not hard-fail a print that physically fits.
|
||||
// Post-generation the mesh bottom already includes the real brim, so the exact
|
||||
// footprint is tested.
|
||||
if (filaments_count > 1 || print.enable_timelapse_print()) {
|
||||
// The shared printable polygon is plate-local, while the tower polygons above are
|
||||
// already shifted by the plate origin.
|
||||
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
|
||||
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
|
||||
for (Polygon &p : printable_polys)
|
||||
p.translate(plate_shift);
|
||||
if (!diff(convex_hulls_temp, printable_polys).empty())
|
||||
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -3408,7 +3435,11 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<
|
||||
}
|
||||
else if ((extruder_volume_type_count > extruder_count) && (m_config.filament_volume_map.values.size() > index))
|
||||
nozzle_volume_type = (NozzleVolumeType)(m_config.filament_volume_map.values[index]);
|
||||
m_config.filament_map_2.values[index] = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
|
||||
// Orca: when the process variant columns cannot be matched (degenerate
|
||||
// print_extruder_id), key the override by plain extruder index like the seeding
|
||||
// above instead of poisoning the map with -1.
|
||||
int slot_index = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
|
||||
m_config.filament_map_2.values[index] = slot_index >= 0 ? slot_index : f_maps[index] - 1;
|
||||
}
|
||||
|
||||
m_full_print_config = m_ori_full_print_config;
|
||||
@@ -3605,7 +3636,8 @@ std::vector<std::set<int>> Print::get_physical_unprintable_filaments(const std::
|
||||
return physical_unprintables;
|
||||
|
||||
auto get_unprintable_extruder_id = [&](unsigned int filament_idx) -> int {
|
||||
int status = m_config.filament_printable.values[filament_idx];
|
||||
// filament_printable may be shorter than the filament count; get_at() clamps.
|
||||
int status = m_config.filament_printable.get_at(filament_idx);
|
||||
for (int i = 0; i < extruder_num; ++i) {
|
||||
if (!(status >> i & 1)) {
|
||||
return i;
|
||||
@@ -3912,6 +3944,12 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
|
||||
double volume = wipe_volume * filament_depth_count;
|
||||
if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2);
|
||||
|
||||
// Sizing should take into account currently set wiping volumes.
|
||||
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
|
||||
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
|
||||
const bool semm_flush = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
|
||||
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
|
||||
|
||||
if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) {
|
||||
double depth = std::sqrt(volume / layer_height * extra_spacing);
|
||||
if (need_wipe_tower || filaments_cnt > 1) {
|
||||
@@ -3923,30 +3961,16 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
|
||||
}
|
||||
}
|
||||
else {
|
||||
double width = m_config.prime_tower_width;
|
||||
if (m_config.purge_in_prime_tower && m_config.single_extruder_multi_material) {
|
||||
// Calculating depth should take into account currently set wiping volumes.
|
||||
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
|
||||
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
|
||||
std::vector<std::vector<float>> wipe_volumes = WipeTower2::extract_wipe_volumes(m_config);
|
||||
std::vector<float> max_wipe_volumes;
|
||||
for (const std::vector<float> &v : wipe_volumes)
|
||||
max_wipe_volumes.emplace_back(*std::max_element(v.begin(), v.end()));
|
||||
float maximum = std::accumulate(max_wipe_volumes.begin(), max_wipe_volumes.end(), 0.f);
|
||||
maximum = maximum * filaments_cnt / max_wipe_volumes.size();
|
||||
|
||||
// Orca: it's overshooting a bit, so let's reduce it a bit
|
||||
maximum *= 0.6;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.depth = maximum / (layer_height * width);
|
||||
} else {
|
||||
double depth = volume / (layer_height * width) * extra_spacing;
|
||||
if (need_wipe_tower || m_wipe_tower_data.depth > EPSILON) {
|
||||
double width = m_config.prime_tower_width;
|
||||
double depth = volume / (layer_height * width);
|
||||
// The flush volumes already hold the spacing between wipes.
|
||||
if (!semm_flush) depth *= extra_spacing;
|
||||
if (need_wipe_tower || depth > EPSILON) {
|
||||
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
|
||||
depth = std::max((double) min_wipe_tower_depth, depth);
|
||||
}
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
|
||||
}
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
|
||||
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
|
||||
}
|
||||
if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height);
|
||||
}
|
||||
@@ -4015,8 +4039,33 @@ void Print::_make_wipe_tower()
|
||||
// in BBL machine, wipe tower is only use to prime extruder. So just use a global wipe volume.
|
||||
WipeTower wipe_tower(m_config, m_plate_index, m_origin, m_wipe_tower_data.tool_ordering.first_extruder(),
|
||||
m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders());
|
||||
// Orca: the tower's first-layer flow follows the user's first-layer flow ratio (BBS reads
|
||||
// its initial_layer_flow_ratio here — STUDIO-14254; first_layer_flow_ratio is Orca's analog,
|
||||
// default 1.0 in both). Honor the set_other_flow_ratios gate that governs the option
|
||||
// everywhere else.
|
||||
wipe_tower.set_first_layer_flow_ratio(m_default_object_config.set_other_flow_ratios
|
||||
? float(m_default_region_config.first_layer_flow_ratio)
|
||||
: 1.f);
|
||||
wipe_tower.set_has_tpu_filament(this->has_tpu_filament());
|
||||
wipe_tower.set_filament_map(this->get_filament_maps());
|
||||
// Per-layer filament->nozzle grouping. sort_and_build_data() above publishes it on the Print
|
||||
// for by-layer prints; by-object prints publish only later (psSkirtBrim), so fall back to the
|
||||
// ToolOrdering's own copy there. set_extruder() below dereferences it, so it must be set first.
|
||||
auto print_group_result = get_layered_nozzle_group_result();
|
||||
const MultiNozzleUtils::LayeredNozzleGroupResult &nozzle_group_result =
|
||||
print_group_result ? *print_group_result : m_wipe_tower_data.tool_ordering.get_layered_nozzle_group_result();
|
||||
wipe_tower.set_nozzle_group_result(nozzle_group_result);
|
||||
{
|
||||
// Orca: acceleration options are object-scope (PrintConfig members in BBS), so resolve
|
||||
// the per-variant columns here; initial_layer_travel_acceleration is FloatOrPercent
|
||||
// over travel_acceleration and needs the full config to resolve.
|
||||
std::vector<double> first_layer_travel_accels;
|
||||
for (size_t i = 0; i < m_config.initial_layer_travel_acceleration.values.size(); ++i)
|
||||
first_layer_travel_accels.emplace_back(m_full_print_config.get_abs_value_at("initial_layer_travel_acceleration", i));
|
||||
wipe_tower.set_accelerations(m_default_object_config.default_acceleration.values,
|
||||
m_default_object_config.initial_layer_acceleration.values,
|
||||
m_default_object_config.travel_acceleration.values,
|
||||
first_layer_travel_accels);
|
||||
}
|
||||
// Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from
|
||||
// the full config — no shipping profile sets it) and the shared printable bed used by the PETG
|
||||
// pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set.
|
||||
@@ -4052,14 +4101,24 @@ void Print::_make_wipe_tower()
|
||||
multi_extruder_flush.emplace_back(wipe_volumes);
|
||||
}
|
||||
|
||||
// Per-carousel-slot purge tracking via NozzleStatusRecorder (BBS pattern); the layered
|
||||
// group result set on the tower above resolves each filament to its nozzle slot per layer.
|
||||
MultiNozzleUtils::NozzleStatusRecorder nozzle_recorder;
|
||||
|
||||
std::vector<int>filament_maps = get_filament_maps();
|
||||
int layer_idx = -1;
|
||||
|
||||
std::vector<unsigned int> nozzle_cur_filament_ids(nozzle_nums, -1);
|
||||
unsigned int current_filament_id = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
size_t cur_nozzle_id = filament_maps[current_filament_id] - 1;
|
||||
nozzle_cur_filament_ids[cur_nozzle_id] = current_filament_id;
|
||||
// Initialize NozzleStatusRecorder with the first filament's carousel slot
|
||||
{
|
||||
auto nozzle = nozzle_group_result.get_nozzle_for_filament(current_filament_id, layer_idx);
|
||||
if (nozzle)
|
||||
nozzle_recorder.set_nozzle_status(nozzle->group_id, current_filament_id, nozzle->extruder_id);
|
||||
}
|
||||
|
||||
for (auto& layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { // for all layers
|
||||
++layer_idx;
|
||||
|
||||
if (!layer_tools.has_wipe_tower) continue;
|
||||
bool first_layer = &layer_tools == &m_wipe_tower_data.tool_ordering.front();
|
||||
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, current_filament_id);
|
||||
@@ -4070,30 +4129,52 @@ void Print::_make_wipe_tower()
|
||||
if (filament_id == current_filament_id)
|
||||
continue;
|
||||
|
||||
int nozzle_id = filament_maps[filament_id] - 1;
|
||||
unsigned int pre_filament_id = nozzle_cur_filament_ids[nozzle_id];
|
||||
|
||||
float volume_to_purge = 0;
|
||||
if (pre_filament_id != (unsigned int)(-1) && pre_filament_id != filament_id) {
|
||||
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
|
||||
// Fast purge mode uses flush_multiplier_fast; Default is inert.
|
||||
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? m_config.flush_multiplier_fast.get_at(nozzle_id)
|
||||
: m_config.flush_multiplier.get_at(nozzle_id);
|
||||
volume_to_purge *= flush_multiplier;
|
||||
volume_to_purge = pre_filament_id == -1 ? 0 :
|
||||
layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge);
|
||||
|
||||
// Per-carousel-slot purge tracking via NozzleStatusRecorder
|
||||
{
|
||||
auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_id, layer_idx);
|
||||
if (nozzle_info) {
|
||||
int extruder_id = nozzle_info->extruder_id;
|
||||
int nozzle_id = nozzle_info->group_id;
|
||||
int prev_nozzle_filament = nozzle_recorder.get_filament_in_nozzle(nozzle_id);
|
||||
|
||||
if (!nozzle_recorder.is_nozzle_empty(nozzle_id) &&
|
||||
static_cast<int>(filament_id) != prev_nozzle_filament) {
|
||||
volume_to_purge = multi_extruder_flush[extruder_id][prev_nozzle_filament][filament_id];
|
||||
// Fast purge mode uses flush_multiplier_fast; Default is inert.
|
||||
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast)
|
||||
? m_config.flush_multiplier_fast.get_at(extruder_id)
|
||||
: m_config.flush_multiplier.get_at(extruder_id);
|
||||
volume_to_purge *= flush_multiplier;
|
||||
volume_to_purge = layer_tools.wiping_extrusions().mark_wiping_extrusions(
|
||||
*this, current_filament_id, filament_id, volume_to_purge);
|
||||
}
|
||||
nozzle_recorder.set_nozzle_status(nozzle_id, filament_id, extruder_id);
|
||||
}
|
||||
}
|
||||
|
||||
//During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length.
|
||||
float grab_purge_volume = m_config.grab_length.get_at(nozzle_id) * 2.4; //(diameter/2)^2*PI=2.4
|
||||
int grab_extruder_id = filament_maps[filament_id] - 1;
|
||||
float grab_purge_volume = m_config.grab_length.get_at(grab_extruder_id) * 2.4; //(diameter/2)^2*PI=2.4
|
||||
volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume);
|
||||
|
||||
// Saving mode reduces the prime volume to 15 mm3; Default is inert.
|
||||
float prime_volume = (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) ? 15.f : (float) m_config.prime_volume;
|
||||
// Prime volume per-filament: the tower now picks extruder-change vs nozzle-change
|
||||
// (carousel) internally per plan layer, so pass both candidates (BBS pattern).
|
||||
float wipe_volume_ec = filament_id < m_config.filament_prime_volume.values.size()
|
||||
? m_config.filament_prime_volume.values[filament_id]
|
||||
: (float) m_config.prime_volume;
|
||||
float wipe_volume_nc = filament_id < m_config.filament_prime_volume_nc.values.size()
|
||||
? m_config.filament_prime_volume_nc.values[filament_id]
|
||||
: (float) m_config.prime_volume;
|
||||
if (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) {
|
||||
wipe_volume_ec = 15.f;
|
||||
wipe_volume_nc = 15.f;
|
||||
}
|
||||
|
||||
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id,
|
||||
prime_volume, volume_to_purge);
|
||||
wipe_volume_ec, wipe_volume_nc, volume_to_purge);
|
||||
current_filament_id = filament_id;
|
||||
nozzle_cur_filament_ids[nozzle_id] = filament_id;
|
||||
}
|
||||
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
|
||||
|
||||
@@ -4270,7 +4351,12 @@ void Print::_make_wipe_tower()
|
||||
wipe_tower.get_rib_width(), wipe_tower.get_rib_length(),
|
||||
config().wipe_tower_fillet_wall.value);
|
||||
const Vec3d origin = Vec3d::Zero();
|
||||
m_fake_wipe_tower.set_fake_extrusion_data(wipe_tower.position(), wipe_tower.width(), wipe_tower.get_wipe_tower_height(),
|
||||
// FakeWipeTower::pos is a bed-frame translation applied after rotation
|
||||
// (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the
|
||||
// tower-local rib offset must be rotated into the bed frame first.
|
||||
m_fake_wipe_tower.rib_offset = Eigen::Rotation2Df(Geometry::deg2rad((float)config().wipe_tower_rotation_angle.value)) *
|
||||
wipe_tower.get_rib_offset();
|
||||
m_fake_wipe_tower.set_fake_extrusion_data(wipe_tower.position() + m_fake_wipe_tower.rib_offset, wipe_tower.width(), wipe_tower.get_wipe_tower_height(),
|
||||
config().initial_layer_print_height, m_wipe_tower_data.depth,
|
||||
m_wipe_tower_data.z_and_depth_pairs, m_wipe_tower_data.brim_width,
|
||||
config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle,
|
||||
|
||||
@@ -559,9 +559,11 @@ static inline bool model_volume_solid_or_modifier(const ModelVolume &mv)
|
||||
|
||||
static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo)
|
||||
{
|
||||
Transform3d m = object_trafo * volume_trafo;
|
||||
m.translation().x() = 0.;
|
||||
m.translation().y() = 0.;
|
||||
// Orca: Keep the volume's local XY offset for multipart overlap checks, but remove the object's bed placement.
|
||||
Transform3d object_trafo_local = object_trafo;
|
||||
object_trafo_local.translation().x() = 0.;
|
||||
object_trafo_local.translation().y() = 0.;
|
||||
Transform3d m = object_trafo_local * volume_trafo;
|
||||
return m.cast<float>();
|
||||
}
|
||||
|
||||
@@ -1355,7 +1357,11 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
if ((extruder_volume_type_count > extruder_count) && opt_filament_volume_maps
|
||||
&& opt_filament_volume_maps->values.size() == filament_maps.size())
|
||||
nozzle_volume_type = (NozzleVolumeType)(opt_filament_volume_maps->values[index]);
|
||||
m_config.filament_map_2.values[index] = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
|
||||
// Orca: when the process variant columns cannot be matched (degenerate
|
||||
// print_extruder_id), key the override by plain extruder index like the seeding
|
||||
// above instead of poisoning the map with -1.
|
||||
int slot_index = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
|
||||
m_config.filament_map_2.values[index] = slot_index >= 0 ? slot_index : filament_maps[index] - 1;
|
||||
}
|
||||
|
||||
// Do not use the ApplyStatus as we will use the max function when updating apply_status.
|
||||
@@ -1411,6 +1417,16 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
|
||||
num_extruders_changed = true;
|
||||
}
|
||||
}
|
||||
else if (! print_diff.empty()) {
|
||||
// Orca: m_config can diverge from an unchanged full config (e.g. the in-slice retract
|
||||
// override recompute writing different values than the apply-time computation). The
|
||||
// invalidation above already fired for print_diff, so repair m_config here as well;
|
||||
// otherwise the divergence is never corrected and every subsequent apply of the same
|
||||
// config invalidates the result again, forever.
|
||||
m_placeholder_parser.apply_config(filament_overrides);
|
||||
m_config.apply_only(new_full_config, print_diff, true);
|
||||
m_config.apply(filament_overrides);
|
||||
}
|
||||
|
||||
ModelObjectStatusDB model_object_status_db;
|
||||
|
||||
|
||||
+150
-22
@@ -72,6 +72,8 @@ const std::vector<std::string> filament_extruder_override_keys = {
|
||||
"filament_deretraction_speed",
|
||||
"filament_retract_restart_extra", //not in filament_options_with_variant, added on 20250816
|
||||
"filament_retraction_minimum_travel",
|
||||
"filament_retract_length_toolchange",
|
||||
"filament_retract_restart_extra_toolchange",
|
||||
// BBS: floats
|
||||
"filament_wipe_distance",
|
||||
// bools
|
||||
@@ -331,6 +333,8 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintSequence)
|
||||
static t_config_enum_values s_keys_map_PrintOrder{
|
||||
{ "default", int(PrintOrder::Default) },
|
||||
{ "as_obj_list", int(PrintOrder::AsObjectList)},
|
||||
{ "best_of", int(PrintOrder::BestOfStrategies)},
|
||||
{ "snake", int(PrintOrder::Snake)},
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintOrder)
|
||||
|
||||
@@ -1081,16 +1085,21 @@ void PrintConfigDef::init_common_params()
|
||||
def->set_default_value(new ConfigOptionString());
|
||||
}
|
||||
|
||||
def = this->add("plugin_config_overrides", coString);
|
||||
def->label = L("Capabilities");
|
||||
def->tooltip = L("Configuration for the plugin capabilities this preset uses, overriding the global "
|
||||
"Capabilities configuration. Stored as a raw JSON array and edited through the dialog "
|
||||
"behind the button, never typed in directly.");
|
||||
// Never shown as a text field: GUIType::plugin_config renders a button that opens PluginsConfigDialog.
|
||||
def->gui_type = ConfigOptionDef::GUIType::plugin_config;
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
// One key per preset type (Preset::plugin_overrides_key), so the print, printer and filament
|
||||
// overrides don't clobber each other when the presets merge into one full config. No handle_legacy
|
||||
// migration from the shared "plugin_config_overrides" they replace: it only ever shipped in
|
||||
// nightlies. Never a text field — GUIType::plugin_config renders a button opening PluginsConfigDialog.
|
||||
for (const char* key : {"print_plugin_config_overrides", "printer_plugin_config_overrides", "filament_plugin_config_overrides"}) {
|
||||
def = this->add(key, coString);
|
||||
def->label = L("Capabilities");
|
||||
def->tooltip = L("Configuration for the plugin capabilities this preset uses, overriding the global "
|
||||
"Capabilities configuration. Stored as a raw JSON array and edited through the dialog "
|
||||
"behind the button, never typed in directly.");
|
||||
def->gui_type = ConfigOptionDef::GUIType::plugin_config;
|
||||
def->mode = comAdvanced;
|
||||
def->cli = ConfigOptionDef::nocli;
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
}
|
||||
}
|
||||
|
||||
void PrintConfigDef::init_fff_params()
|
||||
@@ -1930,6 +1939,13 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(1));
|
||||
|
||||
def = this->add("brim_ears_outer_only", coBool);
|
||||
def->label = L("Brim ears outer only");
|
||||
def->category = L("Support");
|
||||
def->tooltip = L("Generate mouse ears only on the outer contour of the model, excluding holes and enclosed sections.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("compatible_printers", coStrings);
|
||||
def->label = L("Select printers");
|
||||
def->mode = comAdvanced;
|
||||
@@ -1994,12 +2010,30 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("print_order", coEnum);
|
||||
def->label = L("Intra-layer order");
|
||||
def->tooltip = L("Print order within a single layer.");
|
||||
def->tooltip = L("Order in which object instances are visited within a single layer, which controls how much "
|
||||
"travel is spent moving between them.\n\n"
|
||||
"Default: nearest-neighbor chaining, refined with 2-opt and crossing removal. A good general "
|
||||
"choice.\n"
|
||||
"As object list: instances are printed in the same order as the object list, without any path "
|
||||
"optimization. Use it when you need a predictable, manually controlled order.\n"
|
||||
"Best of all (shortest path): every strategy is evaluated and the shortest one is used. The "
|
||||
"object instance order is decided once for the whole print, while the ordering of individual "
|
||||
"islands is decided per layer, so different layers may end up using different strategies. "
|
||||
"Slightly slower to slice.\n"
|
||||
"Snake: serpentine row-by-row traversal, refined with 2-opt. Well suited to regular grids of "
|
||||
"many small parts.\n\n"
|
||||
"With multiple filaments or tools in the same layer, minimizing tool changes takes priority: "
|
||||
"objects are grouped by filament first and this setting only orders the instances within each "
|
||||
"filament group, so the overall sequence may not look like the shortest path across the plate.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<PrintOrder>::get_enum_values();
|
||||
def->enum_values.push_back("default");
|
||||
def->enum_values.push_back("as_obj_list");
|
||||
def->enum_values.push_back("best_of");
|
||||
def->enum_values.push_back("snake");
|
||||
def->enum_labels.push_back(L("Default"));
|
||||
def->enum_labels.push_back(L("As object list"));
|
||||
def->enum_labels.push_back(L("Best of all (shortest path)"));
|
||||
def->enum_labels.push_back(L("Snake"));
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionEnum<PrintOrder>(PrintOrder::Default));
|
||||
|
||||
@@ -3432,6 +3466,17 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back(L("Octagram Spiral"));
|
||||
def->set_default_value(new ConfigOptionEnum<InfillPattern>(ipCrossHatch));
|
||||
|
||||
def = this->add("sparse_infill_smooth_factor", coPercent);
|
||||
def->label = L("Sparse infill smooth factor");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Controls how strongly sparse infill corners are rounded. 0% keeps the original sharp path, "
|
||||
"while 100% produces the largest possible curves between adjacent infill lines.");
|
||||
def->sidetext = "%";
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionPercent(0));
|
||||
|
||||
def = this->add("top_surface_acceleration", coFloats);
|
||||
def->label = L("Top surface");
|
||||
def->category = L("Speed");
|
||||
@@ -4215,6 +4260,15 @@ void PrintConfigDef::init_fff_params()
|
||||
def->readonly = false;
|
||||
def->set_default_value(new ConfigOptionEnum<GCodeFlavor>(gcfMarlinLegacy));
|
||||
|
||||
def = this->add("gcode_skip_config_block", coBool);
|
||||
def->label = L("Skip G-code config block");
|
||||
def->tooltip = L("Do not write the CONFIG_BLOCK (slicer configuration key/value pairs) into the G-code file. "
|
||||
"This can help with printers whose firmware crashes when parsing these comment lines "
|
||||
"(e.g. Anycubic go-klipper). Note: the G-code file will no longer contain slicer settings, "
|
||||
"so importing it back into OrcaSlicer will not restore the configuration.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("pellet_modded_printer", coBool);
|
||||
def->label = L("Pellet Modded Printer");
|
||||
def->tooltip = L("Enable this option if your printer uses pellets instead of filaments.");
|
||||
@@ -4248,7 +4302,7 @@ void PrintConfigDef::init_fff_params()
|
||||
"slow down.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(0));
|
||||
|
||||
|
||||
//BBS
|
||||
def = this->add("infill_combination", coBool);
|
||||
def->label = L("Infill combination");
|
||||
@@ -5624,6 +5678,7 @@ void PrintConfigDef::init_fff_params()
|
||||
// Orca:
|
||||
def = this->add("retract_after_wipe", coPercents);
|
||||
def->label = L("Retract amount after wipe");
|
||||
// xgettext:no-c-format, no-boost-format
|
||||
def->tooltip = L("The length of fast retraction after wipe, relative to retraction length.\n"
|
||||
"The value will be clamped by 100% minus the retract amount before the wipe value.");
|
||||
def->sidetext = "%";
|
||||
@@ -5680,12 +5735,10 @@ void PrintConfigDef::init_fff_params()
|
||||
def->set_default_value(new ConfigOptionFloatsNullable{10});
|
||||
|
||||
def = this->add("retract_length_toolchange", coFloats);
|
||||
def->label = L("Length");
|
||||
//def->full_label = L("Retraction Length (Toolchange)");
|
||||
def->full_label = "Retraction Length (Toolchange)";
|
||||
//def->tooltip = L("When retraction is triggered before changing tool, filament is pulled back "
|
||||
// "by the specified amount (the length is measured on raw filament, before it enters "
|
||||
// "the extruder).");
|
||||
def->label = L("Retraction Length (Toolchange)");
|
||||
def->tooltip = L("When retraction is triggered before changing tool, filament is pulled back "
|
||||
"by the specified amount (the length is measured on raw filament, before it enters "
|
||||
"the extruder).");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloats { 10. });
|
||||
@@ -5939,7 +5992,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->set_default_value(new ConfigOptionFloats { 0. });
|
||||
|
||||
def = this->add("retract_restart_extra_toolchange", coFloats);
|
||||
def->label = L("Extra length on restart");
|
||||
def->label = L("Extra length on restart (Toolchange)");
|
||||
def->tooltip = L("When the retraction is compensated after changing tool, the extruder will push "
|
||||
"this additional amount of filament.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
@@ -6516,6 +6569,17 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("wait_for_temp_on_wipe_tower", coBool);
|
||||
def->label = L("Wait for temperature on wipe tower");
|
||||
def->tooltip = L("Pick up the new tool without waiting for it to reach printing temperature, travel to the wipe "
|
||||
"tower, and wait for the temperature there, right before purging. Ooze from the heat-up lands on "
|
||||
"the tower instead of the model, and the travel overlaps with the heating. "
|
||||
"Only relevant for multi-extruder (multi-toolhead) printers using a Type 2 wipe tower. "
|
||||
"The firmware or tool change macro must not wait for the temperature itself. "
|
||||
"When disabled, the temperature wait is issued right after the tool change command.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
|
||||
def = this->add("wipe_tower_no_sparse_layers", coBool);
|
||||
def->label = L("No sparse layers (beta)");
|
||||
@@ -7384,7 +7448,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def->tooltip = L("The flush multiplier used in fast purge mode.");
|
||||
def->set_default_value(new ConfigOptionFloats{1.2});
|
||||
|
||||
// BBS
|
||||
// Orca: used by the generic (Type2) wipe tower; also the fallback for filament_prime_volume on Type1.
|
||||
def = this->add("prime_volume", coFloat);
|
||||
def->label = L("Prime volume");
|
||||
def->tooltip = L("This is the volume of material to prime the extruder with on the tower.");
|
||||
@@ -8030,6 +8094,16 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comDevelop;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// Used by the Type1 wipe tower: filament_prime_volume on a filament change,
|
||||
// filament_prime_volume_nc on a hotend/nozzle change. Type2 uses prime_volume instead.
|
||||
def = this->add("filament_prime_volume", coFloats);
|
||||
def->label = L("Filament change");
|
||||
def->tooltip = L("The volume of material required to prime the extruder on the tower, excluding a hotend change.");
|
||||
def->sidetext = L("mm³");
|
||||
def->min = 1.0;
|
||||
def->mode = comSimple;
|
||||
def->set_default_value(new ConfigOptionFloats{45.});
|
||||
|
||||
def = this->add("filament_prime_volume_nc", coFloats);
|
||||
def->label = L("Hotend change");
|
||||
def->tooltip = L("The volume of material required to prime the extruder for a hotend change on the tower.");
|
||||
@@ -8099,10 +8173,12 @@ void PrintConfigDef::init_extruder_option_keys()
|
||||
"long_retractions_when_cut",
|
||||
"retract_after_wipe",
|
||||
"retract_before_wipe",
|
||||
"retract_length_toolchange",
|
||||
"retract_lift_above",
|
||||
"retract_lift_below",
|
||||
"retract_lift_enforce",
|
||||
"retract_restart_extra",
|
||||
"retract_restart_extra_toolchange",
|
||||
"retract_when_changing_layer",
|
||||
"retraction_distances_when_cut",
|
||||
"retraction_length",
|
||||
@@ -9050,7 +9126,7 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
|
||||
"retraction_distance_when_cut",
|
||||
"internal_bridge_support_thickness", "top_area_threshold", "reduce_wall_solid_infill","filament_load_time","filament_unload_time",
|
||||
"smooth_coefficient", "overhang_totally_speed", "silent_mode",
|
||||
"overhang_speed_classic", "filament_prime_volume",
|
||||
"overhang_speed_classic",
|
||||
"anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
|
||||
};
|
||||
|
||||
@@ -9190,6 +9266,8 @@ std::set<std::string> filament_options_with_variant = {
|
||||
"filament_retract_lift_below",
|
||||
"filament_retract_lift_enforce",
|
||||
"filament_retract_restart_extra",
|
||||
"filament_retract_length_toolchange",
|
||||
"filament_retract_restart_extra_toolchange",
|
||||
"filament_retraction_speed",
|
||||
"filament_deretraction_speed",
|
||||
"filament_retraction_minimum_travel",
|
||||
@@ -10347,6 +10425,16 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
|
||||
|
||||
}
|
||||
|
||||
void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source,
|
||||
const std::vector<int> &variant_index, int stride)
|
||||
{
|
||||
// A single-value object or region override applies to every nozzle variant.
|
||||
std::vector<int> indices = variant_index;
|
||||
if (source.size() == 1 && !source.is_nil(0))
|
||||
std::fill(indices.begin(), indices.end(), 0);
|
||||
target.set_to_index(&source, indices, stride);
|
||||
}
|
||||
|
||||
|
||||
//used for object/region config
|
||||
//use the smallest of multiple to single
|
||||
@@ -10480,6 +10568,44 @@ int DynamicPrintConfig::get_extruder_nozzle_volume_count(int extruder_count, std
|
||||
return count;
|
||||
}
|
||||
|
||||
// Orca: BBL system profiles ship full-width print_extruder_id/print_extruder_variant columns, but
|
||||
// custom multi-extruder printers only ever get the machine-scope columns synthesized for them (see
|
||||
// extend_extruder_variant); the process scope keeps the length-1 defaults, both in presets and in
|
||||
// 3mf project configs. Expanding with that degenerate map makes every per-extruder lookup fail, and
|
||||
// because both keys are themselves in print_options_with_variant, the expansion then latches a
|
||||
// full-width-but-wrong [1,1,...] map that also defeats the generated_extruder_id fallback in
|
||||
// get_index_for_extruder. Synthesize the process columns from the printer's extruder_variant_list
|
||||
// (same token walk as extend_extruder_variant) before expanding.
|
||||
static void ensure_process_variant_columns(DynamicPrintConfig &config, const DynamicPrintConfig &printer_config)
|
||||
{
|
||||
auto id_opt = dynamic_cast<ConfigOptionInts *>(config.option("print_extruder_id"));
|
||||
auto variant_opt = dynamic_cast<ConfigOptionStrings *>(config.option("print_extruder_variant"));
|
||||
auto list_opt = dynamic_cast<const ConfigOptionStrings *>(printer_config.option("extruder_variant_list"));
|
||||
if (!id_opt || !variant_opt || !list_opt)
|
||||
return;
|
||||
if (id_opt->values.size() != 1 || variant_opt->values.size() != 1)
|
||||
return;
|
||||
|
||||
std::vector<int> ids;
|
||||
std::vector<std::string> variants;
|
||||
for (int i = 0; i < int(list_opt->values.size()); ++i) {
|
||||
std::vector<std::string> tokens;
|
||||
boost::split(tokens, list_opt->get_at(i), boost::is_any_of(","), boost::token_compress_on);
|
||||
for (std::string &token : tokens) {
|
||||
boost::trim(token);
|
||||
if (token.empty())
|
||||
continue;
|
||||
ids.push_back(i + 1);
|
||||
variants.push_back(token);
|
||||
}
|
||||
}
|
||||
// A single column is the legitimate single-extruder layout, not a degenerate one.
|
||||
if (ids.size() <= 1)
|
||||
return;
|
||||
id_opt->values = std::move(ids);
|
||||
variant_opt->values = std::move(variants);
|
||||
}
|
||||
|
||||
std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::vector<std::vector<NozzleVolumeType>>& nv_types,
|
||||
std::set<std::string>& key_set, std::string id_name, std::string variant_name, unsigned int stride, unsigned int extruder_id, NozzleVolumeType filament_nvt)
|
||||
{
|
||||
@@ -10521,6 +10647,8 @@ std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicP
|
||||
variant_count = 1;
|
||||
}
|
||||
else {
|
||||
if (id_name == "print_extruder_id")
|
||||
ensure_process_variant_columns(*this, printer_config);
|
||||
// Orca: emit the slots first, then size variant_count from what was actually
|
||||
// emitted. extruder_nozzle_volume_count only equals the emitted total when every
|
||||
// extruder carries per-type stats; an extruder with an empty stats entry combined
|
||||
@@ -11384,7 +11512,7 @@ void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPr
|
||||
else {
|
||||
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(opt_src);
|
||||
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(opt_dest);
|
||||
opt_vec_src->set_to_index(opt_vec_dest, variant_index, stride);
|
||||
set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index, stride);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,29 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
|
||||
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
|
||||
// than one line per infill wall; a single line makes them plain crossing lines with nothing to round.
|
||||
inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline = 1)
|
||||
{
|
||||
switch (pattern) {
|
||||
case ipHilbertCurve:
|
||||
case ipOctagramSpiral:
|
||||
case ipLightning:
|
||||
case ipHoneycomb:
|
||||
case ip3DHoneycomb:
|
||||
case ipConcentric:
|
||||
case ipCrossHatch:
|
||||
return true;
|
||||
case ipGrid:
|
||||
case ipTriangles:
|
||||
case ipStars:
|
||||
return multiline > 1;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
enum class IroningType {
|
||||
NoIroning,
|
||||
TopSurfaces,
|
||||
@@ -214,6 +237,8 @@ enum class PrintOrder
|
||||
{
|
||||
Default,
|
||||
AsObjectList,
|
||||
BestOfStrategies, // run all custom strategies, pick the shortest total path
|
||||
Snake, // snake-like row traversal (back-and-forth) + 2-opt
|
||||
Count,
|
||||
};
|
||||
|
||||
@@ -840,6 +865,9 @@ extern std::set<std::string> printer_options_with_variant_1;
|
||||
extern std::set<std::string> printer_options_with_variant_2;
|
||||
extern std::set<std::string> empty_options;
|
||||
|
||||
void set_variant_override(ConfigOptionVectorBase &target, const ConfigOptionVectorBase &source,
|
||||
const std::vector<int> &variant_index, int stride = 1);
|
||||
|
||||
extern std::set<std::string> filament_dev_options;
|
||||
|
||||
extern void update_static_print_config_from_dynamic(ConfigBase& config, const DynamicPrintConfig& dest_config, std::vector<int> variant_index, std::set<std::string>& key_set1, int stride = 1);
|
||||
@@ -1080,6 +1108,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionFloat, brim_width))
|
||||
((ConfigOptionFloat, brim_ears_detection_length))
|
||||
((ConfigOptionFloat, brim_ears_max_angle))
|
||||
((ConfigOptionBool, brim_ears_outer_only))
|
||||
((ConfigOptionFloat, skirt_start_angle))
|
||||
((ConfigOptionBool, bridge_no_support))
|
||||
((ConfigOptionFloat, elefant_foot_compensation))
|
||||
@@ -1262,6 +1291,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionString, sparse_infill_rotate_template))
|
||||
((ConfigOptionPercent, sparse_infill_density))
|
||||
((ConfigOptionEnum<InfillPattern>, sparse_infill_pattern))
|
||||
((ConfigOptionPercent, sparse_infill_smooth_factor))
|
||||
((ConfigOptionFloat, lateral_lattice_angle_1))
|
||||
((ConfigOptionFloat, lateral_lattice_angle_2))
|
||||
((ConfigOptionFloat, infill_overhang_angle))
|
||||
@@ -1543,7 +1573,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, gcode_add_line_number))
|
||||
((ConfigOptionBool, bbl_bed_temperature_gcode))
|
||||
((ConfigOptionEnum<GCodeFlavor>, gcode_flavor))
|
||||
|
||||
((ConfigOptionBool, gcode_skip_config_block))
|
||||
((ConfigOptionFloat, time_cost))
|
||||
((ConfigOptionString, layer_change_gcode))
|
||||
((ConfigOptionString, time_lapse_gcode))
|
||||
@@ -1656,6 +1686,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, purge_in_prime_tower))
|
||||
((ConfigOptionBool, enable_filament_ramming))
|
||||
((ConfigOptionBool, tool_change_on_wipe_tower))
|
||||
((ConfigOptionBool, wait_for_temp_on_wipe_tower))
|
||||
((ConfigOptionBool, support_multi_bed_types))
|
||||
((ConfigOptionBool, use_3mf))
|
||||
|
||||
@@ -1780,6 +1811,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
((ConfigOptionString, filename_format))
|
||||
((ConfigOptionStrings, post_process))
|
||||
((ConfigOptionStrings, slicing_pipeline_plugin))
|
||||
((ConfigOptionString, print_plugin_config_overrides))
|
||||
((ConfigOptionString, printer_model))
|
||||
((ConfigOptionFloat, resolution))
|
||||
((ConfigOptionFloats, retraction_minimum_travel))
|
||||
@@ -1841,6 +1873,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
// BBS: wipe tower is only used for priming
|
||||
((ConfigOptionFloat, prime_volume))
|
||||
// Nozzle-change (nc) prime volume + pre-heat delta
|
||||
((ConfigOptionFloats, filament_prime_volume))
|
||||
((ConfigOptionFloats, filament_prime_volume_nc))
|
||||
((ConfigOptionFloatsNullable, filament_preheat_temperature_delta))
|
||||
((ConfigOptionFloats, flush_multiplier))
|
||||
@@ -2387,6 +2420,55 @@ static void set_flush_volumes_matrix(std::vector<T> &out_matrix, const std::vect
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static bool has_zero_flush_volume_for_used_filaments(const std::vector<T> &fv_matrix,
|
||||
const std::vector<T> &flush_multipliers,
|
||||
const std::vector<int> &used_filaments)
|
||||
{
|
||||
if (used_filaments.size() < 2 || flush_multipliers.empty())
|
||||
return false;
|
||||
|
||||
if (fv_matrix.size() % flush_multipliers.size() != 0)
|
||||
return false;
|
||||
|
||||
const size_t matrix_len = fv_matrix.size() / flush_multipliers.size();
|
||||
const size_t row_len = size_t(std::sqrt(double(matrix_len)));
|
||||
if (row_len < 2 || row_len * row_len != matrix_len)
|
||||
return false;
|
||||
|
||||
std::vector<int> filtered_filaments;
|
||||
filtered_filaments.reserve(used_filaments.size());
|
||||
for (int filament_id : used_filaments) {
|
||||
if (filament_id <= 0 || filament_id > int(row_len))
|
||||
continue;
|
||||
if (std::find(filtered_filaments.begin(), filtered_filaments.end(), filament_id) == filtered_filaments.end())
|
||||
filtered_filaments.push_back(filament_id);
|
||||
}
|
||||
if (filtered_filaments.size() < 2)
|
||||
return false;
|
||||
|
||||
for (T multiplier : flush_multipliers) {
|
||||
if (multiplier == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
for (size_t nozzle_idx = 0; nozzle_idx < flush_multipliers.size(); nozzle_idx++) {
|
||||
const size_t block_offset = nozzle_idx * matrix_len;
|
||||
for (int from_id : filtered_filaments) {
|
||||
for (int to_id : filtered_filaments) {
|
||||
if (from_id == to_id)
|
||||
continue;
|
||||
|
||||
const size_t matrix_idx = block_offset + size_t(from_id - 1) * row_len + size_t(to_id - 1);
|
||||
if (matrix_idx < fv_matrix.size() && fv_matrix[matrix_idx] == 0)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t get_extruder_index(const GCodeConfig& config, unsigned int filament_id);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1175,6 +1175,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "brim_type"
|
||||
|| opt_key == "brim_ears_max_angle"
|
||||
|| opt_key == "brim_ears_detection_length"
|
||||
|| opt_key == "brim_ears_outer_only"
|
||||
// BBS: brim generation depends on printing speed
|
||||
|| opt_key == "outer_wall_speed"
|
||||
|| opt_key == "small_perimeter_speed"
|
||||
@@ -1364,7 +1365,6 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_combination_max_layer_height"
|
||||
|| opt_key == "bottom_shell_thickness"
|
||||
|| opt_key == "top_shell_thickness"
|
||||
|| opt_key == "top_surface_expansion"
|
||||
|| opt_key == "top_surface_expansion_margin"
|
||||
|| opt_key == "top_surface_expansion_direction"
|
||||
|| opt_key == "minimum_sparse_infill_area"
|
||||
@@ -1400,7 +1400,6 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_anchor"
|
||||
|| opt_key == "infill_anchor_max"
|
||||
|| opt_key == "top_surface_line_width"
|
||||
|| opt_key == "top_surface_density"
|
||||
|| opt_key == "bottom_surface_density"
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
|| opt_key == "separated_infills"
|
||||
@@ -1411,6 +1410,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_overhang_angle") {
|
||||
steps.emplace_back(posInfill);
|
||||
} else if (opt_key == "sparse_infill_pattern"
|
||||
|| opt_key == "sparse_infill_smooth_factor"
|
||||
|| opt_key == "symmetric_infill_y_axis"
|
||||
|| opt_key == "infill_shift_step"
|
||||
|| opt_key == "sparse_infill_rotate_template"
|
||||
@@ -1434,6 +1434,24 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
is_approx(new_density->value, 0.) || is_approx(new_density->value, 100.))
|
||||
steps.emplace_back(posPerimeters);
|
||||
steps.emplace_back(posPrepareInfill);
|
||||
} else if (opt_key == "top_surface_density") {
|
||||
// ORCA: 0% means no top solid fill, which switches off both the top surface expansion and the wall
|
||||
// removal over top surfaces. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
|
||||
const auto *old_density = old_config.option<ConfigOptionPercent>(opt_key);
|
||||
const auto *new_density = new_config.option<ConfigOptionPercent>(opt_key);
|
||||
assert(old_density && new_density);
|
||||
if (is_approx(old_density->value, 0.) || is_approx(new_density->value, 0.))
|
||||
steps.emplace_back(posPerimeters);
|
||||
steps.emplace_back(posInfill);
|
||||
} else if (opt_key == "top_surface_expansion") {
|
||||
// ORCA: without the expansion the top fill never reaches the space freed by only_one_wall_top, so the
|
||||
// walls over top surfaces are kept. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
|
||||
const auto *old_expansion = old_config.option<ConfigOptionFloat>(opt_key);
|
||||
const auto *new_expansion = new_config.option<ConfigOptionFloat>(opt_key);
|
||||
assert(old_expansion && new_expansion);
|
||||
if (old_expansion->value <= 0. || new_expansion->value <= 0.)
|
||||
steps.emplace_back(posPerimeters);
|
||||
steps.emplace_back(posPrepareInfill);
|
||||
} else if (opt_key == "internal_solid_infill_line_width") {
|
||||
// This value is used for calculating perimeter - infill overlap, thus perimeters need to be recalculated.
|
||||
steps.emplace_back(posPerimeters);
|
||||
@@ -1760,51 +1778,50 @@ void PrintObject::detect_surfaces_type()
|
||||
}
|
||||
}
|
||||
|
||||
// ORCA: Expand the top surfaces outward by top_surface_expansion in every direction. This
|
||||
// enlarges the top solid infill and, in particular, grows it over the covered material left
|
||||
// by features rising from the middle of a top surface (filling holes and joining tops so the
|
||||
// features rest on it). The expansion stays inside the section it belongs to: each connected
|
||||
// solid island has its own outer wall, so the top is grown within each island separately and
|
||||
// clipped to it - growing one island's top across the gap into another island (which may have
|
||||
// no top surface, leaving a partially filled layer) is never allowed. The top infill sits
|
||||
// inside the perimeters, so the margin is measured from the walls: the island is inset by the
|
||||
// band the walls consume (outer wall + inner walls) plus the configured margin, making that
|
||||
// value the real clearance between the expanded top and the walls (avoiding a hull line). The
|
||||
// original top is unioned back in, so where it already sits within that band it is kept as-is.
|
||||
// Never claims a bottom surface.
|
||||
const double top_expansion = layerm->region().config().top_surface_expansion.value;
|
||||
if (top_expansion > 0. && ! top.empty()) {
|
||||
const double d = scale_(top_expansion);
|
||||
const auto jt = Clipper2Lib::JoinType::Miter;
|
||||
const ExPolygons T = union_ex(to_expolygons(top));
|
||||
const int wall_loops = layerm->region().config().wall_loops.value;
|
||||
// ORCA: Grow the top surfaces by top_surface_expansion, so the top solid infill also covers the
|
||||
// material left by features rising from the middle of a top surface (filling the holes and
|
||||
// joining the tops, so the features rest on solid infill). Each connected island is grown and
|
||||
// clipped separately: growing one island's top across a gap into another - which may have no top
|
||||
// surface at all, leaving a partially filled layer - is never allowed. The original top is
|
||||
// unioned back in and bottom surfaces are never claimed, so this can only add area.
|
||||
const PrintRegionConfig ®ion_config = layerm->region().config();
|
||||
const double top_expansion = region_config.top_surface_expansion.value;
|
||||
// Nothing to expand without a top fill: a 0% top surface density leaves the top layer with
|
||||
// walls only, and zero top shell layers retypes it as internal in prepare_fill_surfaces().
|
||||
if (top_expansion > 0. && region_config.top_shell_layers.value > 0 &&
|
||||
region_config.top_surface_density.value > 0. && ! top.empty()) {
|
||||
const double d = scale_(top_expansion);
|
||||
const ExPolygons T = union_ex(to_expolygons(top));
|
||||
// Walls are laid out on spacing, not width; and only_one_wall_top leaves a single wall over
|
||||
// a top surface, which is exactly the situation handled here.
|
||||
const int wall_loops = region_config.only_one_wall_top.value ? std::min(region_config.wall_loops.value, 1)
|
||||
: region_config.wall_loops.value;
|
||||
const double wall_band = wall_loops <= 0 ? 0. :
|
||||
double(layerm->flow(frExternalPerimeter).scaled_width()) +
|
||||
double(layerm->flow(frPerimeter).scaled_width()) * double(wall_loops - 1);
|
||||
const double margin = scale_(layerm->region().config().top_surface_expansion_margin.value);
|
||||
double(layerm->flow(frPerimeter).scaled_spacing()) * double(wall_loops - 1);
|
||||
const double margin = scale_(region_config.top_surface_expansion_margin.value);
|
||||
// minimum real top to act on: ignore anything thinner than ~2 top-infill lines
|
||||
const float min_top = float(layerm->flow(frTopSolidInfill).scaled_width());
|
||||
const auto direction = layerm->region().config().top_surface_expansion_direction.value;
|
||||
const auto direction = region_config.top_surface_expansion_direction.value;
|
||||
|
||||
ExPolygons grown;
|
||||
for (const ExPolygon &island : union_ex(layerm_slices_surfaces)) {
|
||||
// The top infill only exists inside the perimeters, so seed and measure from the infill
|
||||
// region (the island minus the wall band), not the raw slice. A section whose only
|
||||
// exposed top lies in the wall band - i.e. a layer where the top is just the walls
|
||||
// themselves - has no infill here and is skipped, instead of being flooded inward by
|
||||
// the expansion. Thin slivers inside the infill region are dropped by the opening too.
|
||||
// region (the island minus the wall band), not the raw slice: a section whose exposed top
|
||||
// is just the walls themselves is then skipped instead of being flooded inward. Clip the
|
||||
// layer's tops to the island first, to keep the boolean ops proportional to the island.
|
||||
const ExPolygons infill_region = wall_band > 0. ? offset_ex(island, -float(wall_band)) : ExPolygons{ island };
|
||||
const ExPolygons island_top = intersection_ex(T, infill_region);
|
||||
const ExPolygons island_top = intersection_ex(
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(T, get_extents(island).inflated(SCALED_EPSILON)),
|
||||
infill_region);
|
||||
if (opening_ex(island_top, min_top).empty())
|
||||
continue; // no real top infill in this section - never expand into it
|
||||
|
||||
// grow by d, then keep only the part allowed by the configured direction: inward fills
|
||||
// the holes/gaps left by features (clip the growth back to the top's own filled outline,
|
||||
// which leaves the outer edge fixed), outward grows the outer edge toward the walls (drop
|
||||
// the growth that fell into the original holes), and inward+outward keeps both.
|
||||
ExPolygons expanded = offset_ex_2(island_top, d, jt);
|
||||
// Grow, then keep only what the configured direction allows, using the top's own filled
|
||||
// outline (same outer edge, holes closed) to tell the two apart.
|
||||
ExPolygons expanded = offset_ex_2(island_top, d, Clipper2Lib::JoinType::Miter);
|
||||
if (direction != TopSurfaceExpansionDirection::InwardAndOutward) {
|
||||
ExPolygons outline; // the top with its holes filled (same outer edge)
|
||||
ExPolygons outline;
|
||||
outline.reserve(island_top.size());
|
||||
for (const ExPolygon &ex : island_top)
|
||||
outline.emplace_back(ex.contour);
|
||||
@@ -3795,7 +3812,7 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
|
||||
else {
|
||||
ConfigOptionVectorBase* opt_vec_src = static_cast<ConfigOptionVectorBase*>(my_opt);
|
||||
const ConfigOptionVectorBase* opt_vec_dest = static_cast<const ConfigOptionVectorBase*>(it->second.get());
|
||||
opt_vec_src->set_to_index(opt_vec_dest, variant_index, 1);
|
||||
set_variant_override(*opt_vec_src, *opt_vec_dest, variant_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +56,12 @@ public:
|
||||
int & i,
|
||||
Eigen::Matrix<double, 1, 3> &closest)
|
||||
{
|
||||
size_t idx_unsigned = 0;
|
||||
Vec3d closest_vec3d(closest);
|
||||
double dist =
|
||||
size_t idx_unsigned { 0 };
|
||||
Vec3d closest_vec3d { Vec3d::Zero() };
|
||||
const double dist {
|
||||
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
|
||||
its.vertices, its.indices, m_tree, point, idx_unsigned,
|
||||
closest_vec3d);
|
||||
closest_vec3d) };
|
||||
i = int(idx_unsigned);
|
||||
closest = closest_vec3d;
|
||||
return dist;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "KDTreeIndirect.hpp"
|
||||
#include "MutablePriorityQueue.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "GCode/OrderingStrategies.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
@@ -1103,7 +1104,7 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy) {
|
||||
return chain_points(points);
|
||||
}
|
||||
|
||||
std::vector<size_t> chain_points(const Points &points, Point *start_near)
|
||||
std::vector<size_t> chain_points(const Points &points, const Point *start_near)
|
||||
{
|
||||
auto segment_end_point = [&points](size_t idx, bool /* first_point */) -> const Point& { return points[idx]; };
|
||||
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, points.size(), start_near);
|
||||
@@ -1111,9 +1112,26 @@ std::vector<size_t> chain_points(const Points &points, Point *start_near)
|
||||
out.reserve(ordered.size());
|
||||
for (auto &segment_and_reversal : ordered)
|
||||
out.emplace_back(segment_and_reversal.first);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
|
||||
{
|
||||
std::vector<size_t> path = chain_points(points, start_near);
|
||||
// Alternate 2-opt and crossing removal until convergence.
|
||||
// 2-opt can create new crossings, and crossing removal can create new
|
||||
// opportunities for 2-opt improvement. Break early if neither improves.
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
bool improved = tsp_2opt_improve(path, points);
|
||||
improved |= tsp_remove_crossings(path, points);
|
||||
if (!improved) break;
|
||||
}
|
||||
if (start_near == nullptr)
|
||||
tsp_rotate_minimize_closing(path, points);
|
||||
return path;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
// #define DEBUG_SVG_OUTPUT
|
||||
#endif /* NDEBUG */
|
||||
@@ -2025,12 +2043,13 @@ std::vector<const PrintInstance*> chain_print_object_instances(const std::vector
|
||||
instances.emplace_back(i, j);
|
||||
}
|
||||
}
|
||||
auto segment_end_point = [&object_reference_points](size_t idx, bool /* first_point */) -> const Point& { return object_reference_points[idx]; };
|
||||
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, instances.size(), start_near);
|
||||
// Order objects using nearest neighbor + post-processing (crossing removal + 2-opt).
|
||||
std::vector<size_t> path = chain_points_with_postprocessing(object_reference_points, start_near);
|
||||
|
||||
std::vector<const PrintInstance*> out;
|
||||
out.reserve(instances.size());
|
||||
for (auto& segment_and_reversal : ordered) {
|
||||
const std::pair<size_t, size_t>& inst = instances[segment_and_reversal.first];
|
||||
out.reserve(path.size());
|
||||
for (size_t idx : path) {
|
||||
const std::pair<size_t, size_t>& inst = instances[idx];
|
||||
out.emplace_back(&print_objects[inst.first]->instances()[inst.second]);
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace Slic3r {
|
||||
using PolyNodes = std::vector<PolyNode*, PointsAllocator<PolyNode*>>;
|
||||
}
|
||||
|
||||
std::vector<size_t> chain_points(const Points &points, Point *start_near = nullptr);
|
||||
std::vector<size_t> chain_points(const Points &points, const Point *start_near = nullptr);
|
||||
// Variant with post-processing (crossing removal + 2-opt) for object ordering.
|
||||
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near = nullptr);
|
||||
std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
|
||||
|
||||
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
|
||||
|
||||
@@ -65,6 +65,15 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
|
||||
const bool smooth_supports = support_params.support_style != smsGrid;
|
||||
SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first;
|
||||
SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second;
|
||||
// The user-facing interface layer counts include the contact layer. Internally,
|
||||
// contact layers are generated separately, so only the remaining layers are
|
||||
// projected into intermediate interface/base-interface layers here.
|
||||
const size_t num_top_interface_layers = support_params.has_top_contacts ? support_params.num_top_interface_layers - 1 : 0;
|
||||
const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ? support_params.num_bottom_interface_layers - 1 : 0;
|
||||
const size_t num_top_base_interface_layers = std::min(support_params.num_top_base_interface_layers, num_top_interface_layers);
|
||||
const size_t num_bottom_base_interface_layers = std::min(support_params.num_bottom_base_interface_layers, num_bottom_interface_layers);
|
||||
const size_t num_top_interface_layers_only = num_top_interface_layers - num_top_base_interface_layers;
|
||||
const size_t num_bottom_interface_layers_only = num_bottom_interface_layers - num_bottom_base_interface_layers;
|
||||
|
||||
interface_layers.assign(intermediate_layers.size(), nullptr);
|
||||
if (support_params.has_base_interfaces())
|
||||
@@ -124,6 +133,8 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
|
||||
};
|
||||
tbb::parallel_for(tbb::blocked_range<int>(0, int(intermediate_layers.size())),
|
||||
[&bottom_contacts, &top_contacts, &top_interface_layers, &top_base_interface_layers, &intermediate_layers, &insert_layer, &support_params,
|
||||
num_top_interface_layers, num_bottom_interface_layers, num_top_base_interface_layers, num_bottom_base_interface_layers,
|
||||
num_top_interface_layers_only, num_bottom_interface_layers_only,
|
||||
snug_supports, &interface_layers, &base_interface_layers](const tbb::blocked_range<int>& range) {
|
||||
// Gather the top / bottom contact layers intersecting with num_interface_layers resp. num_interface_layers_only intermediate layers above / below
|
||||
// this intermediate layer.
|
||||
@@ -142,16 +153,16 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
|
||||
Polygons polygons_top_contact_projected_base;
|
||||
Polygons polygons_bottom_contact_projected_interface;
|
||||
Polygons polygons_bottom_contact_projected_base;
|
||||
if (support_params.num_top_interface_layers > 0) {
|
||||
if (num_top_interface_layers > 0) {
|
||||
// Top Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
|
||||
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers) - 1)]->print_z;
|
||||
coordf_t top_inteface_z = std::numeric_limits<coordf_t>::max();
|
||||
if (support_params.num_top_base_interface_layers > 0)
|
||||
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers) - 1)]->print_z;
|
||||
coordf_t top_interface_z = std::numeric_limits<coordf_t>::max();
|
||||
if (num_top_base_interface_layers > 0)
|
||||
// Some top base interface layers will be generated.
|
||||
top_inteface_z = support_params.num_top_interface_layers_only() == 0 ?
|
||||
top_interface_z = num_top_interface_layers_only == 0 ?
|
||||
// Only base interface layers to generate.
|
||||
- std::numeric_limits<coordf_t>::max() :
|
||||
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers_only()) - 1)]->print_z;
|
||||
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers_only) - 1)]->print_z;
|
||||
// Move idx_top_contact_first up until above the current print_z.
|
||||
idx_top_contact_first = idx_higher_or_equal(top_contacts, idx_top_contact_first, [&intermediate_layer](const SupportGeneratorLayer *layer){ return layer->print_z >= intermediate_layer.print_z; }); // - EPSILON
|
||||
// Collect the top contact areas above this intermediate layer, below top_z.
|
||||
@@ -160,22 +171,22 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
|
||||
//FIXME maybe this adds one interface layer in excess?
|
||||
if (top_contact_layer.bottom_z - EPSILON > top_z)
|
||||
break;
|
||||
polygons_append(top_contact_layer.bottom_z - EPSILON > top_inteface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
|
||||
polygons_append(top_contact_layer.bottom_z - EPSILON > top_interface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
|
||||
// For snug supports, project the overhang polygons covering the whole overhang, so that they will merge without a gap with support polygons of the other layers.
|
||||
// For grid supports, merging of support regions will be performed by the projection into grid.
|
||||
snug_supports ? *top_contact_layer.overhang_polygons : top_contact_layer.polygons);
|
||||
}
|
||||
}
|
||||
if (support_params.num_bottom_interface_layers > 0) {
|
||||
if (num_bottom_interface_layers > 0) {
|
||||
// Bottom Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
|
||||
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers) + 1)]->bottom_z;
|
||||
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers) + 1)]->bottom_z;
|
||||
coordf_t bottom_interface_z = - std::numeric_limits<coordf_t>::max();
|
||||
if (support_params.num_bottom_base_interface_layers > 0)
|
||||
if (num_bottom_base_interface_layers > 0)
|
||||
// Some bottom base interface layers will be generated.
|
||||
bottom_interface_z = support_params.num_bottom_interface_layers_only() == 0 ?
|
||||
bottom_interface_z = num_bottom_interface_layers_only == 0 ?
|
||||
// Only base interface layers to generate.
|
||||
std::numeric_limits<coordf_t>::max() :
|
||||
intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers_only()))]->bottom_z;
|
||||
intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers_only))]->bottom_z;
|
||||
// Move idx_bottom_contact_first up until touching bottom_z.
|
||||
idx_bottom_contact_first = idx_higher_or_equal(bottom_contacts, idx_bottom_contact_first, [bottom_z](const SupportGeneratorLayer *layer){ return layer->print_z >= bottom_z - EPSILON; });
|
||||
// Collect the top contact areas above this intermediate layer, below top_z.
|
||||
@@ -1563,13 +1574,17 @@ void generate_support_toolpaths(
|
||||
// Pointer to the 1st layer interface filler.
|
||||
auto filler_first_layer = filler_first_layer_ptr ? filler_first_layer_ptr.get() : filler_interface.get();
|
||||
// Filler for the 1st layer interface, if different from filler_interface.
|
||||
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && config.support_interface_top_layers.value == 0 ?
|
||||
const bool top_interfaces_enabled = support_params.num_top_interface_layers > 0;
|
||||
const bool bottom_interfaces_enabled = support_params.num_bottom_interface_layers > 0;
|
||||
const coordf_t base_interface_density = top_interfaces_enabled || !bottom_interfaces_enabled ?
|
||||
support_params.top_interface_density : support_params.bottom_interface_density;
|
||||
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && !top_interfaces_enabled ?
|
||||
Fill::new_from_type(support_params.raft_interface_fill_pattern) : nullptr);
|
||||
// Pointer to the 1st layer interface filler.
|
||||
auto filler_raft_contact = filler_raft_contact_ptr ? filler_raft_contact_ptr.get() : filler_interface.get();
|
||||
// Filler for the base interface (to be used for soluble interface / non soluble base, to produce non soluble interface layer below soluble interface layer).
|
||||
auto filler_base_interface = std::unique_ptr<Fill>(base_interface_layers.empty() ? nullptr :
|
||||
Fill::new_from_type(support_params.top_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
|
||||
Fill::new_from_type(base_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
|
||||
auto filler_support = std::unique_ptr<Fill>(Fill::new_from_type(support_params.base_fill_pattern));
|
||||
filler_interface->set_bounding_box(bbox_object);
|
||||
if (filler_first_layer_ptr)
|
||||
@@ -1583,10 +1598,7 @@ void generate_support_toolpaths(
|
||||
{
|
||||
SupportLayer &support_layer = *support_layers[support_layer_id];
|
||||
LayerCache &layer_cache = layer_caches[support_layer_id];
|
||||
const float support_interface_angle = (config.support_interface_pattern == smipRectilinearInterlaced) ?
|
||||
support_params.raft_interface_angle(support_layer.interface_id()) :
|
||||
((support_params.support_style == smsGrid || config.support_interface_pattern == smipRectilinear) ?
|
||||
support_params.interface_angle : support_params.raft_interface_angle(support_layer.interface_id()));
|
||||
const float support_interface_angle = support_params.support_interface_angle(support_layer.interface_id());
|
||||
|
||||
// Find polygons with the same print_z.
|
||||
SupportGeneratorLayerExtruded &bottom_contact_layer = layer_cache.bottom_contact_layer;
|
||||
@@ -1619,7 +1631,9 @@ void generate_support_toolpaths(
|
||||
bool raft_layer = slicing_params.interface_raft_layers && top_contact_layer.layer && is_approx(top_contact_layer.layer->print_z, slicing_params.raft_contact_top_z);
|
||||
// ORCA: Organic tree uses projected contacts to build the interface stack; avoid extra bottom-contact extrusion.
|
||||
const bool organic_tree = support_params.support_style == SupportMaterialStyle::smsTreeOrganic;
|
||||
if (config.support_interface_top_layers == 0) {
|
||||
const bool top_interfaces = support_params.num_top_interface_layers > 0;
|
||||
const bool bottom_interfaces = support_params.num_bottom_interface_layers > 0;
|
||||
if (!top_interfaces) {
|
||||
// If no top interface layers were requested, we treat the contact layer exactly as a generic base layer.
|
||||
// Don't merge the raft contact layer though.
|
||||
if (support_params.can_merge_support_regions && ! raft_layer) {
|
||||
@@ -1642,15 +1656,29 @@ void generate_support_toolpaths(
|
||||
if (top_contact_layer.could_merge(interface_layer) && ! raft_layer)
|
||||
top_contact_layer.merge(std::move(interface_layer));
|
||||
}
|
||||
if ((config.support_interface_top_layers == 0 || config.support_interface_bottom_layers == 0) && support_params.can_merge_support_regions) {
|
||||
if (!bottom_interfaces && support_params.can_merge_support_regions) {
|
||||
if (base_layer.could_merge(bottom_contact_layer))
|
||||
base_layer.merge(std::move(bottom_contact_layer));
|
||||
else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging)
|
||||
base_layer = std::move(bottom_contact_layer);
|
||||
} else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) {
|
||||
top_contact_layer.merge(std::move(bottom_contact_layer));
|
||||
if (top_interfaces && bottom_interfaces) {
|
||||
top_contact_layer.merge(std::move(bottom_contact_layer));
|
||||
} else if (bottom_interfaces) {
|
||||
top_contact_layer.set_polygons_to_extrude(
|
||||
diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude()));
|
||||
} else {
|
||||
bottom_contact_layer.set_polygons_to_extrude(
|
||||
diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude()));
|
||||
}
|
||||
} else if (bottom_contact_layer.could_merge(interface_layer) && ! organic_tree) {
|
||||
bottom_contact_layer.merge(std::move(interface_layer));
|
||||
const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface;
|
||||
if (bottom_interfaces && interface_layer_is_bottom) {
|
||||
bottom_contact_layer.merge(std::move(interface_layer));
|
||||
} else {
|
||||
bottom_contact_layer.set_polygons_to_extrude(
|
||||
diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude()));
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: For organic trees the support-material regions are generated from
|
||||
@@ -1730,12 +1758,12 @@ void generate_support_toolpaths(
|
||||
interface_as_base ? ExtrusionRole::erSupportMaterial : ExtrusionRole::erSupportMaterialInterface, interface_flow);
|
||||
}
|
||||
};
|
||||
const bool top_interfaces = support_params.num_top_interface_layers > 0;
|
||||
const bool bottom_interfaces = top_interfaces && support_params.num_bottom_interface_layers > 0;
|
||||
extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase);
|
||||
if (!organic_tree)
|
||||
extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase);
|
||||
extrude_interface(interface_layer, top_interfaces ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
|
||||
const bool interface_layer_enabled = !interface_layer.empty() &&
|
||||
(interface_layer.layer->layer_type == SupporLayerType::BottomInterface ? bottom_interfaces : top_interfaces);
|
||||
extrude_interface(interface_layer, interface_layer_enabled ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
|
||||
// Base interface layers under soluble interfaces
|
||||
if ( ! base_interface_layer.empty() && ! base_interface_layer.polygons_to_extrude().empty()) {
|
||||
Fill *filler = filler_base_interface.get();
|
||||
@@ -1745,7 +1773,7 @@ void generate_support_toolpaths(
|
||||
Flow interface_flow = support_params.support_material_flow.with_height(float(base_interface_layer.layer->height));
|
||||
filler->angle = support_interface_angle;
|
||||
filler->spacing = support_params.support_material_interface_flow.spacing();
|
||||
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.top_interface_density));
|
||||
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / base_interface_density));
|
||||
fill_expolygons_generate_paths(
|
||||
// Destination
|
||||
base_interface_layer.extrusions,
|
||||
@@ -1753,7 +1781,7 @@ void generate_support_toolpaths(
|
||||
// Regions to fill
|
||||
union_safety_offset_ex(base_interface_layer.polygons_to_extrude()),
|
||||
// Filler and its parameters
|
||||
filler, float(support_params.top_interface_density),
|
||||
filler, float(base_interface_density),
|
||||
// Extrusion parameters
|
||||
ExtrusionRole::erSupportMaterial, interface_flow);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ struct SupportParameters {
|
||||
|
||||
{
|
||||
this->num_top_interface_layers = std::max(0, object_config.support_interface_top_layers.value);
|
||||
this->num_bottom_interface_layers = number_of_support_interface_bottom_layers(object_config);
|
||||
this->num_bottom_interface_layers = std::max(0, number_of_support_interface_bottom_layers(object_config));
|
||||
this->has_top_contacts = num_top_interface_layers > 0;
|
||||
this->has_bottom_contacts = num_bottom_interface_layers > 0;
|
||||
// BBS: if support interface and support base do not use the same filament, add a base layer to improve their adhesion
|
||||
@@ -46,15 +46,15 @@ struct SupportParameters {
|
||||
if (non_soluble_base_top) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
|
||||
this->num_top_base_interface_layers = size_t(std::min(int(num_top_interface_layers) / 2, 2));
|
||||
} else {
|
||||
this->num_top_base_interface_layers =
|
||||
(different_support_interface_filament && this->zero_gap_interface_top) ? 1 : 0;
|
||||
// Keep at least one configured layer on the interface filament.
|
||||
this->num_top_base_interface_layers = different_support_interface_filament && num_top_interface_layers > 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
if (non_soluble_base_bottom) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
|
||||
this->num_bottom_base_interface_layers = size_t(std::min(int(num_bottom_interface_layers) / 2, 2));
|
||||
} else {
|
||||
this->num_bottom_base_interface_layers =
|
||||
(different_support_interface_filament && this->zero_gap_interface_bottom) ? 1 : 0;
|
||||
// Keep at least one configured layer on the interface filament.
|
||||
this->num_bottom_base_interface_layers = different_support_interface_filament && num_bottom_interface_layers > 1 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height));
|
||||
@@ -74,7 +74,7 @@ struct SupportParameters {
|
||||
for (auto layer : object.layers())
|
||||
this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, layer->height));
|
||||
|
||||
if (object_config.support_interface_top_layers.value == 0) {
|
||||
if (this->num_top_interface_layers == 0 && this->num_bottom_interface_layers == 0) {
|
||||
// No interface layers allowed, print everything with the base support pattern.
|
||||
this->support_material_interface_flow = this->support_material_flow;
|
||||
}
|
||||
@@ -120,8 +120,8 @@ struct SupportParameters {
|
||||
this->raft_interface_density = std::min(1., this->raft_interface_flow.spacing() / raft_interface_spacing);
|
||||
this->support_spacing = object_config.support_base_pattern_spacing.value + this->support_material_flow.spacing();
|
||||
this->support_density = std::min(1., this->support_material_flow.spacing() / this->support_spacing);
|
||||
if (object_config.support_interface_top_layers.value == 0) {
|
||||
// No interface layers allowed, print everything with the base support pattern.
|
||||
if (this->num_top_interface_layers == 0) {
|
||||
// No top interface layers allowed; keep unused top interface parameters aligned with base support.
|
||||
this->top_interface_spacing = this->support_spacing;
|
||||
this->top_interface_density = this->support_density;
|
||||
}
|
||||
@@ -133,16 +133,20 @@ struct SupportParameters {
|
||||
this->support_density > 0.95 || this->with_sheath ? ipRectilinear : ipSupportBase;
|
||||
this->interface_fill_pattern = (this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
|
||||
this->raft_interface_fill_pattern = this->raft_interface_density > 0.95 ? ipRectilinear : ipSupportBase;
|
||||
const coordf_t contact_interface_density = this->num_top_interface_layers > 0 ?
|
||||
this->top_interface_density : this->bottom_interface_density;
|
||||
const bool zero_gap_contact_interface = this->num_top_interface_layers > 0 ?
|
||||
this->zero_gap_interface_top : this->zero_gap_interface_bottom;
|
||||
if (object_config.support_interface_pattern == smipGrid)
|
||||
this->contact_fill_pattern = ipGrid;
|
||||
else if (object_config.support_interface_pattern == smipRectilinearInterlaced)
|
||||
this->contact_fill_pattern = ipRectilinear;
|
||||
else
|
||||
this->contact_fill_pattern =
|
||||
(object_config.support_interface_pattern == smipAuto && this->zero_gap_interface_top) ||
|
||||
(object_config.support_interface_pattern == smipAuto && zero_gap_contact_interface) ||
|
||||
object_config.support_interface_pattern == smipConcentric ?
|
||||
ipConcentric :
|
||||
(this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
|
||||
(contact_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
|
||||
|
||||
this->raft_angle_1st_layer = 0.f;
|
||||
this->raft_angle_base = 0.f;
|
||||
@@ -188,6 +192,7 @@ struct SupportParameters {
|
||||
std::numeric_limits<double>::max();
|
||||
|
||||
support_style = object_config.support_style;
|
||||
support_interface_pattern = object_config.support_interface_pattern;
|
||||
if (support_style != smsDefault) {
|
||||
if ((support_style == smsSnug || support_style == smsGrid) && is_tree(object_config.support_type)) support_style = smsDefault;
|
||||
if ((support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid || support_style == smsTreeOrganic) &&
|
||||
@@ -211,9 +216,9 @@ struct SupportParameters {
|
||||
bool has_top_contacts;
|
||||
// Is there at least a bottom contact layer extruded below support base?
|
||||
bool has_bottom_contacts;
|
||||
// Number of top interface layers without counting the contact layer.
|
||||
// User-configured number of top interface layers, including the contact layer.
|
||||
size_t num_top_interface_layers;
|
||||
// Number of bottom interface layers without counting the contact layer.
|
||||
// User-configured number of bottom interface layers, including the contact layer.
|
||||
size_t num_bottom_interface_layers;
|
||||
// Number of top base interface layers.
|
||||
size_t num_top_base_interface_layers;
|
||||
@@ -235,7 +240,7 @@ struct SupportParameters {
|
||||
Flow support_material_interface_flow;
|
||||
// Flow at the bottom interfaces and contacts.
|
||||
Flow support_material_bottom_interface_flow;
|
||||
// Flow at raft inteface & contact layers.
|
||||
// Flow at raft interface & contact layers.
|
||||
Flow raft_interface_flow;
|
||||
coordf_t support_extrusion_width;
|
||||
// Is merging of regions allowed? Could the interface & base support regions be printed with the same extruder?
|
||||
@@ -262,6 +267,7 @@ struct SupportParameters {
|
||||
// Density of the base support layers.
|
||||
coordf_t support_density;
|
||||
SupportMaterialStyle support_style = smsDefault;
|
||||
SupportMaterialInterfacePattern support_interface_pattern = smipAuto;
|
||||
|
||||
// Pattern of the sparse infill including sparse raft layers.
|
||||
InfillPattern base_fill_pattern;
|
||||
@@ -280,9 +286,33 @@ struct SupportParameters {
|
||||
float raft_angle_base;
|
||||
float raft_angle_interface;
|
||||
|
||||
// Produce a raft interface angle for a given SupportLayer::interface_id()
|
||||
// Produce a +/-45deg alternating raft interface angle for a given SupportLayer::interface_id().
|
||||
float raft_interface_angle(size_t interface_id) const
|
||||
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); }
|
||||
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI_4) : float(+ M_PI_4)); }
|
||||
|
||||
// Produce support interface angle for a given SupportLayer::interface_id().
|
||||
// Angle will be shifted/rotated based on interface pattern.
|
||||
float support_interface_angle(size_t interface_id) const
|
||||
{
|
||||
float angle;
|
||||
|
||||
switch (this->support_interface_pattern) {
|
||||
case SupportMaterialInterfacePattern::smipRectilinear:
|
||||
angle = support_style == SupportMaterialStyle::smsSnug ? this->interface_angle - float(M_PI_4) : this->interface_angle;
|
||||
break;
|
||||
case SupportMaterialInterfacePattern::smipRectilinearInterlaced:
|
||||
angle = this->interface_angle + ((interface_id & 1) ? float(M_PI_4) : float(-M_PI_4));
|
||||
break;
|
||||
case SupportMaterialInterfacePattern::smipGrid:
|
||||
angle = this->base_angle;
|
||||
break;
|
||||
default:
|
||||
angle = this->interface_angle;
|
||||
break;
|
||||
}
|
||||
|
||||
return angle;
|
||||
}
|
||||
|
||||
bool independent_layer_height = false;
|
||||
const double thresh_big_overhang = Slic3r::sqr(scale_(10));
|
||||
|
||||
@@ -469,7 +469,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
|
||||
});
|
||||
|
||||
// 2) Sum over top / bottom ranges.
|
||||
const bool processing_last_mesh = outline_idx == layer_outline_indices.size();
|
||||
const bool processing_last_mesh = outline_idx == layer_outline_indices.back();
|
||||
tbb::parallel_for(tbb::blocked_range<LayerIndex>(data.begin(), data.end()),
|
||||
[&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius,
|
||||
xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel]
|
||||
|
||||
@@ -1511,7 +1511,9 @@ void TreeSupport::generate_toolpaths()
|
||||
// ORCA: reset interface Fill state per area group to keep angles deterministic.
|
||||
filler_interface->fixed_angle = false;
|
||||
filler_interface->layer_id = size_t(-1);
|
||||
filler_interface->angle = base_support_angle + M_PI_2; // default interface angle is perpendicular to support angle
|
||||
filler_Roof1stLayer->fixed_angle = false;
|
||||
filler_Roof1stLayer->layer_id = size_t(-1);
|
||||
filler_interface->angle = m_support_params.support_interface_angle(area_group.interface_id);
|
||||
if (area_group.type != SupportLayer::BaseType) {
|
||||
// interface
|
||||
if (layer_id == 0) {
|
||||
@@ -1537,8 +1539,10 @@ void TreeSupport::generate_toolpaths()
|
||||
fill_params.density = interface_density;
|
||||
// Note: spacing means the separation between two lines as if they are tightly extruded
|
||||
filler_Roof1stLayer->spacing = interface_flow.spacing();
|
||||
filler_Roof1stLayer->angle = base_support_angle;
|
||||
filler_Roof1stLayer->angle = m_support_params.support_interface_angle(area_group.interface_id);
|
||||
fill_params.dont_sort = true;
|
||||
filler_Roof1stLayer->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
|
||||
m_object_config->support_interface_pattern == smipRectilinear);
|
||||
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
|
||||
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
|
||||
// generate a perimeter first to support interface better
|
||||
@@ -1556,18 +1560,11 @@ void TreeSupport::generate_toolpaths()
|
||||
fill_params.density = bottom_interface_density;
|
||||
filler_interface->spacing = interface_flow.spacing();
|
||||
|
||||
if (m_object_config->support_interface_pattern == smipGrid) {
|
||||
filler_interface->angle = base_support_angle;
|
||||
fill_params.dont_sort = true;
|
||||
}
|
||||
|
||||
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
|
||||
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
|
||||
filler_interface->fixed_angle = true;
|
||||
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
|
||||
fill_params.dont_sort = true;
|
||||
}
|
||||
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
|
||||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
|
||||
|
||||
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
|
||||
m_object_config->support_interface_pattern == smipRectilinear);
|
||||
|
||||
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
|
||||
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
|
||||
@@ -1579,17 +1576,11 @@ void TreeSupport::generate_toolpaths()
|
||||
fill_params.density = interface_density;
|
||||
filler_interface->spacing = interface_flow.spacing();
|
||||
|
||||
if (m_object_config->support_interface_pattern == smipGrid) {
|
||||
filler_interface->angle = base_support_angle;
|
||||
fill_params.dont_sort = true;
|
||||
}
|
||||
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
|
||||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
|
||||
|
||||
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
|
||||
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
|
||||
filler_interface->fixed_angle = true;
|
||||
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
|
||||
fill_params.dont_sort = true;
|
||||
}
|
||||
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
|
||||
m_object_config->support_interface_pattern == smipRectilinear);
|
||||
|
||||
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
|
||||
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
|
||||
@@ -2014,6 +2005,9 @@ void TreeSupport::draw_circles()
|
||||
// generate areas
|
||||
const coordf_t layer_height = config.layer_height.value;
|
||||
const size_t top_interface_layers = m_support_params.num_top_interface_layers;
|
||||
const int top_base_interface_layers = std::min<int>(
|
||||
int(m_support_params.num_top_base_interface_layers),
|
||||
top_interface_layers > 0 ? int(top_interface_layers) - 1 : 0);
|
||||
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
|
||||
const double nozzle_diameter = m_object->print()->config().nozzle_diameter.get_at(0);
|
||||
const coordf_t line_width = config.get_abs_value("support_line_width", nozzle_diameter);
|
||||
@@ -2054,12 +2048,14 @@ void TreeSupport::draw_circles()
|
||||
|
||||
ExPolygons& base_areas = ts_layer->base_areas;
|
||||
ExPolygons& roof_areas = ts_layer->roof_areas;
|
||||
ExPolygons roof_base_areas;
|
||||
ExPolygons& roof_1st_layer = ts_layer->roof_1st_layer;
|
||||
ExPolygons& floor_areas = ts_layer->floor_areas;
|
||||
ExPolygons& roof_gap_areas = ts_layer->roof_gap_areas;
|
||||
coordf_t max_layers_above_base = 0;
|
||||
coordf_t max_layers_above_roof = 0;
|
||||
coordf_t max_layers_above_roof1 = 0;
|
||||
size_t first_base_roof_area = 0;
|
||||
bool floor_interface_as_base = false;
|
||||
bool has_circle_node = false;
|
||||
bool need_extra_wall = false;
|
||||
@@ -2094,8 +2090,6 @@ void TreeSupport::draw_circles()
|
||||
break;
|
||||
|
||||
const SupportNode& node = *p_node;
|
||||
// ORCA: Cap top interface height in mm based on per-node support layer height.
|
||||
const coordf_t top_interface_height = coordf_t(top_interface_layers) * node.height;
|
||||
ExPolygons area;
|
||||
// Generate directly from overhang polygon if one of the following is true:
|
||||
// 1) node is a normal part of hybrid support
|
||||
@@ -2159,18 +2153,16 @@ void TreeSupport::draw_circles()
|
||||
|
||||
if (obj_layer_nr>0 && node.distance_to_top < 0)
|
||||
append(roof_gap_areas, area);
|
||||
// ORCA: Roof1stLayer must also fit inside the mm cap.
|
||||
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
|
||||
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail==false)
|
||||
node.is_sharp_tail == false)
|
||||
{
|
||||
append(roof_1st_layer, area);
|
||||
max_layers_above_roof1 = std::max(max_layers_above_roof1, node.dist_mm_to_top);
|
||||
}
|
||||
// ORCA: Roof layers must also fit inside the mm cap.
|
||||
else if (obj_layer_nr > 0 && node.support_roof_layers_below > 1 &&
|
||||
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail == false)
|
||||
node.is_sharp_tail == false)
|
||||
{
|
||||
append(roof_areas, area);
|
||||
append(node.support_roof_layers_below <= top_base_interface_layers ? roof_base_areas : roof_areas, area);
|
||||
max_layers_above_roof = std::max(max_layers_above_roof, node.dist_mm_to_top);
|
||||
}
|
||||
else
|
||||
@@ -2184,9 +2176,17 @@ void TreeSupport::draw_circles()
|
||||
//m_object->print()->set_status(65, (boost::format( _u8L("Support: generate polygons at layer %d")) % layer_nr).str());
|
||||
|
||||
// join roof segments
|
||||
roof_areas = diff_clipped(offset2_ex(roof_areas, line_width_scaled, -line_width_scaled), get_collision(false));
|
||||
roof_areas = diff_clipped(closing_ex(roof_areas, line_width_scaled), get_collision(false));
|
||||
roof_areas = intersection_ex(roof_areas, m_machine_border);
|
||||
roof_1st_layer = diff_clipped(offset2_ex(roof_1st_layer, line_width_scaled, -line_width_scaled), get_collision(false));
|
||||
roof_base_areas = diff_clipped(closing_ex(roof_base_areas, line_width_scaled), get_collision(false));
|
||||
roof_base_areas = intersection_ex(roof_base_areas, m_machine_border);
|
||||
if (!roof_base_areas.empty() && !roof_areas.empty())
|
||||
roof_base_areas = diff_ex(roof_base_areas,
|
||||
ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas, get_extents(roof_base_areas)));
|
||||
|
||||
first_base_roof_area = roof_areas.size();
|
||||
append(roof_areas, std::move(roof_base_areas));
|
||||
roof_1st_layer = diff_clipped(closing_ex(roof_1st_layer, line_width_scaled), get_collision(false));
|
||||
|
||||
// roof_1st_layer and roof_areas may intersect, so need to subtract roof_areas from roof_1st_layer
|
||||
roof_1st_layer = diff_ex(roof_1st_layer, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas,get_extents(roof_1st_layer)));
|
||||
@@ -2366,9 +2366,11 @@ void TreeSupport::draw_circles()
|
||||
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
|
||||
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
|
||||
}
|
||||
for (auto& expoly : ts_layer->roof_areas) {
|
||||
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
|
||||
auto &expoly = ts_layer->roof_areas[roof_idx];
|
||||
//if (area(expoly) < SQ(scale_(1))) continue;
|
||||
area_groups.emplace_back(&expoly, SupportLayer::RoofType, max_layers_above_roof);
|
||||
area_groups.back().interface_as_base = roof_idx >= first_base_roof_area;
|
||||
}
|
||||
for (auto &expoly : ts_layer->floor_areas) {
|
||||
//if (area(expoly) < SQ(scale_(1))) continue;
|
||||
@@ -2378,6 +2380,7 @@ void TreeSupport::draw_circles()
|
||||
for (auto &expoly : ts_layer->roof_1st_layer) {
|
||||
//if (area(expoly) < SQ(scale_(1))) continue;
|
||||
area_groups.emplace_back(&expoly, SupportLayer::Roof1stLayer, max_layers_above_roof1);
|
||||
area_groups.back().interface_as_base = top_base_interface_layers > 0;
|
||||
}
|
||||
|
||||
for (auto &area_group : area_groups) {
|
||||
@@ -2406,7 +2409,6 @@ void TreeSupport::draw_circles()
|
||||
}
|
||||
});
|
||||
// ORCA: normalize interface_id sequencing to follow printed interface layers only.
|
||||
const int top_base_layers = int(m_support_params.num_top_base_interface_layers);
|
||||
const bool interlaced = m_object_config->support_interface_pattern == smipRectilinearInterlaced;
|
||||
int roof_interface_id = 0;
|
||||
int floor_interface_id = 0;
|
||||
@@ -2425,7 +2427,6 @@ void TreeSupport::draw_circles()
|
||||
if (area_group.type == SupportLayer::RoofType || area_group.type == SupportLayer::Roof1stLayer) {
|
||||
if (interlaced)
|
||||
area_group.interface_id = roof_interface_id;
|
||||
area_group.interface_as_base = top_base_layers > 0 && roof_interface_id < top_base_layers;
|
||||
has_roof_interface = true;
|
||||
} else if (area_group.type == SupportLayer::FloorType) {
|
||||
if (interlaced)
|
||||
@@ -2897,7 +2898,7 @@ void TreeSupport::drop_nodes()
|
||||
node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node);
|
||||
const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position);
|
||||
SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next,
|
||||
node_parent->support_roof_layers_below - (node_parent->distance_to_top > 0 ? 1 : 0),
|
||||
node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, node_parent, print_z_next, height_next);
|
||||
get_max_move_dist(next_node);
|
||||
m_ts_data->m_mutex.lock();
|
||||
@@ -2949,7 +2950,7 @@ void TreeSupport::drop_nodes()
|
||||
for(auto& overhang:overhangs_next) {
|
||||
Point next_pt = overhang.contour.centroid();
|
||||
SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next,
|
||||
p_node->support_roof_layers_below - (p_node->distance_to_top > 0 ? 1 : 0),
|
||||
p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, p_node, print_z_next, height_next);
|
||||
next_node->max_move_dist = 0;
|
||||
next_node->overhang = std::move(overhang);
|
||||
@@ -3096,7 +3097,7 @@ void TreeSupport::drop_nodes()
|
||||
auto next_collision = get_collision(0, obj_layer_nr_next);
|
||||
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
|
||||
SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next,
|
||||
node.support_roof_layers_below - (node.distance_to_top > 0 ? 1 : 0),
|
||||
node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, p_node, print_z_next, height_next);
|
||||
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
|
||||
to_outside = projection_onto(next_collision, next_node->position);
|
||||
@@ -3376,21 +3377,6 @@ std::vector<LayerHeightData> TreeSupport::plan_layer_heights()
|
||||
}
|
||||
}
|
||||
|
||||
// ORCA: Recompute support_roof_layers_below from remaining interface height (independent heights).
|
||||
const int top_layers = m_object->config().support_interface_top_layers.value;
|
||||
if (m_support_params.independent_layer_height && top_layers > 0) {
|
||||
const coordf_t interface_height_mm = coordf_t(top_layers) * m_slicing_params.layer_height;
|
||||
for (int layer_nr = 0; layer_nr < contact_nodes.size(); layer_nr++) {
|
||||
if (contact_nodes[layer_nr].empty()) continue;
|
||||
for (SupportNode *node : contact_nodes[layer_nr]) {
|
||||
if (node->height <= EPSILON) continue;
|
||||
const coordf_t remaining_mm = interface_height_mm - (node->dist_mm_to_top - this->top_z_distance);
|
||||
const int layers_fit = remaining_mm < -EPSILON ? 0 : int(std::floor((remaining_mm + EPSILON) / node->height));
|
||||
node->support_roof_layers_below = std::min(layers_fit, top_layers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log layer_heights
|
||||
for (size_t i = 0; i < layer_heights.size(); i++) {
|
||||
//if (layer_heights[i].height > EPSILON)
|
||||
@@ -3498,7 +3484,7 @@ void TreeSupport::generate_contact_points()
|
||||
if (force_add || !already_inserted.count(hash_pos)) {
|
||||
already_inserted.emplace(hash_pos);
|
||||
bool to_buildplate = true;
|
||||
size_t roof_layers = add_interface ? (support_roof_layers > 0 ? support_roof_layers - 1 : 0) : 0; // subtract 1 because the contact node itself counts as one layer
|
||||
size_t roof_layers = add_interface ? support_roof_layers : 0;
|
||||
// add a new node as a virtual node which acts as the invisible gap between support and object
|
||||
// distance_to_top=-1: it's virtual
|
||||
// print_z=object_layer->bottom_z: it directly contacts the bottom
|
||||
|
||||
@@ -706,7 +706,7 @@ static std::optional<std::pair<Point, size_t>> polyline_sample_next_point_at_dis
|
||||
filler->spacing = flow.spacing();
|
||||
filler->angle = roof ?
|
||||
//fixme support_layer.interface_id() instead of layer_idx
|
||||
(support_params.interface_angle + (layer_idx & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)) :
|
||||
(support_params.interface_angle + ((layer_idx & 1) ? float(- M_PI_4) : float(+ M_PI_4))) :
|
||||
support_params.base_angle;
|
||||
|
||||
// ORCA: use top-specific interface density after separating top/bottom settings.
|
||||
|
||||
@@ -62,7 +62,7 @@ struct TreeSupportMeshGroupSettings {
|
||||
this->support_line_width = support_material_flow(&print_object, config.layer_height).scaled_width();
|
||||
this->support_roof_line_width = support_material_interface_flow(&print_object, config.layer_height).scaled_width();
|
||||
const int bottom_interface_layers = number_of_support_interface_bottom_layers(config);
|
||||
this->support_bottom_enable = config.support_interface_top_layers.value > 0 && bottom_interface_layers > 0;
|
||||
this->support_bottom_enable = bottom_interface_layers > 0;
|
||||
this->support_bottom_height = this->support_bottom_enable ?
|
||||
bottom_interface_layers * this->layer_height :
|
||||
0;
|
||||
@@ -705,7 +705,7 @@ public:
|
||||
SupportGeneratorLayersPtr& top_contacts_mutable() { return this->top_contacts; }
|
||||
|
||||
public:
|
||||
// Insert the contact layer and some of the inteface and base interface layers below.
|
||||
// Insert the contact layer and some of the interface and base interface layers below.
|
||||
void add_roofs(std::vector<Polygons> &&new_roofs, const size_t insert_layer_idx)
|
||||
{
|
||||
if (! new_roofs.empty()) {
|
||||
|
||||
@@ -771,6 +771,7 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
|
||||
}
|
||||
}
|
||||
|
||||
gcode << m_writer.reset_e();
|
||||
gcode << m_writer.set_pressure_advance(m_params.start);
|
||||
gcode << "; end pressure advance pattern for layer\n";
|
||||
|
||||
|
||||
+16
-4
@@ -35,17 +35,29 @@ struct Calib_Params
|
||||
{
|
||||
Calib_Params() : mode(CalibMode::Calib_None){};
|
||||
int extruder_id = 0;
|
||||
double start, end, step;
|
||||
bool print_numbers;
|
||||
double freqStartX, freqEndX, freqStartY, freqEndY;
|
||||
int test_model;
|
||||
double start = 0.0, end = 1.0, step = 0.1;
|
||||
bool print_numbers = false;
|
||||
double freqStartX = 0.0, freqEndX = 1.0, freqStartY = 0.0, freqEndY = 1.0;
|
||||
int test_model = 0;
|
||||
std::string shaper_type;
|
||||
std::vector<double> accelerations;
|
||||
std::vector<double> speeds;
|
||||
// Resolved layer height for the VFA tower (0 = auto: nozzle_diameter / 2). Each speed block is a
|
||||
// fixed number of layers tall, so this also determines the physical block height / tower height.
|
||||
double vfa_layer_height = 0.0;
|
||||
// Scale the calibration model to the nozzle diameter and set the layer height accordingly (temp tower / VFA).
|
||||
// When false the 0.4 mm / 0.2 mm reference model is printed as-is.
|
||||
bool nozzle_based_resize = true;
|
||||
|
||||
CalibMode mode;
|
||||
};
|
||||
|
||||
// Number of printed layers per speed block in the VFA tower. The base model has 5 mm blocks designed
|
||||
// for a 0.2 mm layer height (0.4 mm nozzle), i.e. 25 layers per block.
|
||||
static constexpr int vfa_layers_per_block = 25;
|
||||
static constexpr double vfa_base_block_height = 5.0;
|
||||
static constexpr double vfa_base_nozzle_diameter = 0.4;
|
||||
|
||||
enum FlowRatioCalibrationType {
|
||||
COMPLETE_CALIBRATION = 0,
|
||||
FINE_CALIBRATION,
|
||||
|
||||
Reference in New Issue
Block a user