mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 03:11:47 +00:00
Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase
# Conflicts: # resources/profiles/Custom.json # src/libslic3r/Brim.cpp # src/libslic3r/GCode.cpp # src/libslic3r/GCode.hpp # src/libslic3r/Preset.cpp # src/slic3r/GUI/3DScene.cpp # src/slic3r/GUI/ConfigManipulation.cpp # src/slic3r/GUI/GLCanvas3D.cpp # src/slic3r/GUI/Plater.cpp
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>();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -280,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);
|
||||
|
||||
@@ -626,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");
|
||||
@@ -847,6 +856,10 @@ std::string AppConfig::load()
|
||||
local_machine.dev_ip = p["dev_ip"].get<std::string>();
|
||||
if (p.contains("printer_type"))
|
||||
local_machine.printer_type = p["printer_type"].get<std::string>();
|
||||
if (p.contains("printer_agent_id"))
|
||||
local_machine.printer_agent_id = p["printer_agent_id"].get<std::string>();
|
||||
if (p.contains("access_code"))
|
||||
local_machine.access_code = p["access_code"].get<std::string>();
|
||||
m_local_machines[local_machine.dev_id] = local_machine;
|
||||
}
|
||||
} else {
|
||||
@@ -877,7 +890,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();
|
||||
@@ -1059,6 +1072,8 @@ void AppConfig::save()
|
||||
m_json["dev_name"] = local_machine.second.dev_name;
|
||||
m_json["dev_ip"] = local_machine.second.dev_ip;
|
||||
m_json["printer_type"] = local_machine.second.printer_type;
|
||||
m_json["printer_agent_id"] = local_machine.second.printer_agent_id;
|
||||
m_json["access_code"] = local_machine.second.access_code;
|
||||
|
||||
j["local_machines"][local_machine.first] = m_json;
|
||||
}
|
||||
@@ -1624,6 +1639,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
|
||||
@@ -61,10 +66,19 @@ struct BBLocalMachine
|
||||
std::string dev_ip;
|
||||
std::string dev_id; /* serial number */
|
||||
std::string printer_type; /* model_id */
|
||||
std::string printer_agent_id; /* id of the IPrinterAgent that discovered/bound this device, e.g. "bbl"; empty for entries persisted before this field existed */
|
||||
// Access code, scoped to printer_agent_id above - so a code saved while bound under one
|
||||
// printer agent isn't treated as valid for a different, independent agent talking to the
|
||||
// same physical dev_id. Empty for entries persisted before this field existed; those fall
|
||||
// back to the legacy flat "access_code"/"user_access_code" AppConfig sections (BBL-only,
|
||||
// since BBL was the only agent when they were saved) - see
|
||||
// get_access_code_with_legacy_fallback() in DevManager.cpp.
|
||||
std::string access_code;
|
||||
|
||||
bool operator==(const BBLocalMachine& other) const
|
||||
{
|
||||
return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type;
|
||||
return dev_name == other.dev_name && dev_ip == other.dev_ip && dev_id == other.dev_id && printer_type == other.printer_type &&
|
||||
printer_agent_id == other.printer_agent_id && access_code == other.access_code;
|
||||
}
|
||||
bool operator!=(const BBLocalMachine& other) const { return !operator==(other); }
|
||||
};
|
||||
@@ -374,6 +388,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;
|
||||
|
||||
@@ -154,8 +154,8 @@ void simplify(Polygon &thiss, const int64_t smallest_line_segment_squared, const
|
||||
//h^2 = L^2 / b^2 [factor the divisor]
|
||||
const int64_t height_2 = double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2);
|
||||
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
|
||||
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) //Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current, previous, next) <= scaled<double>(0.005))) // make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) //Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current, previous, next) <= double(colinear_vertex_tolerance()))) // make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
continue;
|
||||
|
||||
if (length2 < smallest_line_segment_squared
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -133,8 +133,8 @@ void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const
|
||||
const auto height_2 = int64_t(double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2));
|
||||
const int64_t extrusion_area_error = calculateExtrusionAreaDeviationError(previous, current, next);
|
||||
// Orca: The value of `height_2` is squared, so we need to compare it with the squared value
|
||||
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) // Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= scaled<double>(0.005)) // Make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
if ((height_2 <= Slic3r::sqr(colinear_vertex_tolerance()) // Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= double(colinear_vertex_tolerance())) // Make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
// We shouldn't remove middle junctions of colinear segments if the area changed for the C-P segment is exceeding the maximum allowed
|
||||
&& extrusion_area_error <= maximum_extrusion_area_deviation)
|
||||
{
|
||||
|
||||
@@ -32,6 +32,14 @@ class Flow;
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
// ORCA: Tolerance of the "almost exactly colinear" early-out shared by the two simplify() passes
|
||||
// (this file and WallToolPaths.cpp). That test drops a vertex regardless of the user's Maximum wall
|
||||
// resolution/deviation, so it has to stay at the scale of coordinate rounding noise. A larger value
|
||||
// silently decimates finely tessellated curves: on a circle, one vertex may be removed whenever the
|
||||
// sagitta of the resulting chord falls below the tolerance, which halves the point count and turns
|
||||
// smooth arcs into corners the firmware has to decelerate through.
|
||||
inline coord_t colinear_vertex_tolerance() { return coord_t(SCALED_EPSILON); }
|
||||
|
||||
/*!
|
||||
* Represents a polyline (not just a line) that is to be extruded with variable
|
||||
* line width.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+6
-10
@@ -349,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;
|
||||
@@ -373,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));
|
||||
@@ -452,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;
|
||||
// btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
|
||||
// edge, so it degrades to an ordinary outer brim rather than silently to none.
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears;
|
||||
@@ -533,7 +529,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 +543,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);
|
||||
|
||||
@@ -162,6 +162,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
|
||||
@@ -363,6 +365,8 @@ set(lisbslic3r_sources
|
||||
Polyline.hpp
|
||||
PresetBundle.cpp
|
||||
PresetBundle.hpp
|
||||
PresetCacheFormat.cpp
|
||||
PresetCacheFormat.hpp
|
||||
Preset.cpp
|
||||
Preset.hpp
|
||||
PrincipalComponents2D.cpp
|
||||
|
||||
@@ -2031,7 +2031,8 @@ const double& DynamicConfig::opt_float(const t_config_option_key &opt_key, unsig
|
||||
return opt_floats_nullable->get_at(idx);
|
||||
} else {
|
||||
assert(false);
|
||||
return 0;
|
||||
static const double zero = 0.0;
|
||||
return zero;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
|
||||
#include <cereal/access.hpp>
|
||||
#include <cereal/types/base_class.hpp>
|
||||
// The serialize() members below archive ConfigOption hierarchies through
|
||||
// cereal::base_class, whose registration machinery lives in polymorphic.hpp.
|
||||
#include <cereal/types/polymorphic.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
struct FloatOrPercent
|
||||
@@ -2273,6 +2276,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.
|
||||
@@ -2980,6 +2985,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
|
||||
|
||||
@@ -682,7 +682,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
|
||||
return fuzzified;
|
||||
}
|
||||
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour)
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed)
|
||||
{
|
||||
const auto slice_z = perimeter_generator.slice_z;
|
||||
const auto& regions = perimeter_generator.regions_by_fuzzify;
|
||||
@@ -690,7 +690,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
const auto& config = regions.begin()->first;
|
||||
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
|
||||
if (fuzzify)
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, config);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed);
|
||||
} else {
|
||||
// Merge regions that produce identical fuzzy effects (differ only in type).
|
||||
// When the style (e.g. External) and a painted region (All) both fuzzify this loop
|
||||
@@ -701,10 +701,19 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
|
||||
// Fast path: single merged region — apply directly without splitting
|
||||
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) {
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed);
|
||||
return;
|
||||
}
|
||||
|
||||
// Open path means this is a thin wall that collapsed into a single thick line, in this case the path will go exactly
|
||||
// between the middle two sides of the object. And since the paint segmentation never goes beyond the middle line because
|
||||
// it uses voronoi diagram, we need to expand the segmentation a little bit to make sure it covers the path.
|
||||
if (!closed) {
|
||||
for (auto& r : merged_regions) {
|
||||
r.expolygons = offset_ex(r.expolygons, perimeter_generator.ext_perimeter_flow.scaled_width() / 10);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FUZZY
|
||||
{
|
||||
int i = 0;
|
||||
@@ -752,7 +761,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
// Fuzzy splitted extrusion
|
||||
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
|
||||
// The entire polygon is fuzzified
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config);
|
||||
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed);
|
||||
continue;
|
||||
} else {
|
||||
const auto current_ext = extrusion->junctions;
|
||||
@@ -803,7 +812,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
|
||||
}
|
||||
|
||||
//Orca: ensure the loop is closed after fuzzy
|
||||
if (!extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
|
||||
if (closed && !extrusion->junctions.empty() && extrusion->junctions.front().p != extrusion->junctions.back().p) {
|
||||
extrusion->junctions.back().p = extrusion->junctions.front().p;
|
||||
extrusion->junctions.back().w = extrusion->junctions.front().w;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ void group_region_by_fuzzify(PerimeterGenerator& g);
|
||||
bool should_fuzzify(const FuzzySkinConfig& config, int layer_id, size_t loop_idx, bool is_contour);
|
||||
|
||||
Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, size_t loop_idx, bool is_contour);
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour);
|
||||
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, bool is_contour, bool closed = true);
|
||||
|
||||
} // namespace Slic3r::Feature::FuzzySkin
|
||||
|
||||
|
||||
@@ -1021,7 +1021,7 @@ namespace Slic3r
|
||||
if (FGMode::MatchMode == ctx.group_info.mode)
|
||||
return calc_filament_group_for_match(cost);
|
||||
}
|
||||
catch (const FilamentGroupException& e) {
|
||||
catch (const FilamentGroupException&) {
|
||||
}
|
||||
|
||||
return calc_filament_group_for_flush(cost);
|
||||
|
||||
@@ -278,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};
|
||||
|
||||
@@ -316,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);
|
||||
@@ -348,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;
|
||||
}
|
||||
};
|
||||
@@ -964,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.;
|
||||
@@ -1328,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;
|
||||
@@ -1569,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
|
||||
|
||||
@@ -351,19 +351,23 @@ void Node::convertToPolylines(Polylines &output, const coord_t line_overlap) con
|
||||
{
|
||||
Polylines result;
|
||||
result.emplace_back();
|
||||
convertToPolylines(0, result);
|
||||
// Orca: the layers are filled in parallel, so they would consume a shared generator in a
|
||||
// different order every run, and a model would not slice the same way twice. Each tree seeds
|
||||
// its own from where it is rooted; one constant seed would start them all on the same pick.
|
||||
std::mt19937_64 rng { uint64_t(PointHash{}(m_p)) };
|
||||
convertToPolylines(0, result, rng);
|
||||
removeJunctionOverlap(result, line_overlap);
|
||||
append(output, std::move(result));
|
||||
}
|
||||
|
||||
void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
|
||||
void Node::convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const
|
||||
{
|
||||
if (m_children.empty()) {
|
||||
output[long_line_idx].points.push_back(m_p);
|
||||
return;
|
||||
}
|
||||
size_t first_child_idx = rand() % m_children.size();
|
||||
m_children[first_child_idx]->convertToPolylines(long_line_idx, output);
|
||||
const size_t first_child_idx = rng() % m_children.size();
|
||||
m_children[first_child_idx]->convertToPolylines(long_line_idx, output, rng);
|
||||
output[long_line_idx].points.push_back(m_p);
|
||||
|
||||
for (size_t idx_offset = 1; idx_offset < m_children.size(); idx_offset++) {
|
||||
@@ -371,7 +375,7 @@ void Node::convertToPolylines(size_t long_line_idx, Polylines &output) const
|
||||
const Node& child = *m_children[child_idx];
|
||||
output.emplace_back();
|
||||
size_t child_line_idx = output.size() - 1;
|
||||
child.convertToPolylines(child_line_idx, output);
|
||||
child.convertToPolylines(child_line_idx, output, rng);
|
||||
output[child_line_idx].points.emplace_back(m_p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include "../../EdgeGrid.hpp"
|
||||
@@ -259,8 +260,9 @@ protected:
|
||||
*
|
||||
* \param long_line a reference to a polyline in \p output which to continue building on in the recursion
|
||||
* \param output all branches in this tree connected into polylines
|
||||
* \param rng the generator the junctions draw from, carried through the recursion
|
||||
*/
|
||||
void convertToPolylines(size_t long_line_idx, Polylines &output) const;
|
||||
void convertToPolylines(size_t long_line_idx, Polylines &output, std::mt19937_64 &rng) const;
|
||||
|
||||
void removeJunctionOverlap(Polylines &polylines, coord_t line_overlap) const;
|
||||
|
||||
|
||||
@@ -712,7 +712,7 @@ unsigned int Step::get_triangle_num(double linear_deflection, double angle_defle
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
} catch(const Exception &) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+111
-71
@@ -771,30 +771,31 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
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;
|
||||
@@ -901,27 +902,17 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos;
|
||||
}
|
||||
|
||||
// Printable-area bounds for tower-approach routing, in object coordinates (shared by
|
||||
// the BBL avoid-perimeter path in append_tcr and the Type2 skip-points router).
|
||||
// Multi-nozzle: clamp the 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 every existing single/dual printer keeps the historic
|
||||
// full-printable_area routing byte-identical.
|
||||
BoundingBox WipeTowerIntegration::printer_travel_bounds(GCode &gcodegen) const
|
||||
// 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
|
||||
{
|
||||
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
|
||||
BoundingBox printer_bbx;
|
||||
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 {
|
||||
Points bed_points;
|
||||
for (const auto& p : gcodegen.m_config.printable_area.values)
|
||||
bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast<float>() + plate_origin_2d));
|
||||
printer_bbx = BoundingBox(bed_points);
|
||||
}
|
||||
return printer_bbx;
|
||||
// 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
|
||||
@@ -936,15 +927,37 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
if (!WipeTower2::use_gap_wall(gcodegen.m_config))
|
||||
return {};
|
||||
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
|
||||
// Transform the tower-local bbx corners exactly like the tcr points; a rotated
|
||||
// tower gets a conservative axis-aligned envelope.
|
||||
Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon();
|
||||
for (auto& p : avoid_points.points)
|
||||
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
|
||||
BoundingBox avoid_bbx(avoid_points.points);
|
||||
if (avoid_bbx.contains(route_start))
|
||||
// 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 {};
|
||||
Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_travel_bounds(gcodegen));
|
||||
|
||||
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)
|
||||
@@ -1140,8 +1153,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();
|
||||
@@ -1325,7 +1338,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 = printer_travel_bounds(gcodegen);
|
||||
BoundingBox avoid_bbx;
|
||||
{
|
||||
// set avoid_bbx
|
||||
avoid_bbx = scaled(m_wipe_tower_bbx);
|
||||
@@ -1337,7 +1350,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];
|
||||
@@ -1557,7 +1570,8 @@ 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
|
||||
@@ -1708,7 +1722,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;
|
||||
@@ -2932,6 +2946,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
|
||||
@@ -3144,7 +3159,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);
|
||||
@@ -4182,23 +4197,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");
|
||||
@@ -5781,7 +5798,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
|
||||
@@ -7146,6 +7163,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,
|
||||
@@ -8099,8 +8117,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);
|
||||
@@ -8111,7 +8128,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) {
|
||||
@@ -9419,7 +9439,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))
|
||||
@@ -9518,7 +9538,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)
|
||||
@@ -9526,6 +9546,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;
|
||||
@@ -9549,7 +9587,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.
|
||||
@@ -9829,8 +9867,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)) {
|
||||
|
||||
@@ -133,11 +133,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;
|
||||
BoundingBox printer_travel_bounds(GCode &gcodegen) 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;
|
||||
@@ -187,7 +187,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 {
|
||||
@@ -267,7 +267,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();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -298,6 +298,7 @@ void GCodeProcessor::TimeMachine::State::reset()
|
||||
//BBS
|
||||
enter_direction = { 0.0f, 0.0f, 0.0f };
|
||||
exit_direction = { 0.0f, 0.0f, 0.0f };
|
||||
jd_unit_vec = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
}
|
||||
|
||||
void GCodeProcessor::TimeMachine::CustomGCodeTime::reset()
|
||||
@@ -5107,6 +5108,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
if (!is_extrusion_only_move(delta_pos))
|
||||
curr.enter_direction = curr.enter_direction / norm;
|
||||
curr.exit_direction = curr.enter_direction;
|
||||
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
|
||||
static_cast<float>(delta_pos[Y]),
|
||||
static_cast<float>(delta_pos[Z]),
|
||||
static_cast<float>(delta_pos[E])).normalized();
|
||||
|
||||
TimeBlock block;
|
||||
block.move_type = type;
|
||||
@@ -5189,22 +5194,32 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
||||
|
||||
block.acceleration = acceleration;
|
||||
|
||||
// calculates block exit feedrate
|
||||
curr.safe_feedrate = block.feedrate_profile.cruise;
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
|
||||
// Negative leaves the classic jerk path below unchanged.
|
||||
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
|
||||
static_cast<PrintEstimatedStatistics::ETimeMode>(i));
|
||||
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
|
||||
|
||||
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
|
||||
// free to start from rest.
|
||||
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
|
||||
|
||||
if (!use_junction_deviation) {
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
}
|
||||
}
|
||||
|
||||
block.feedrate_profile.exit = curr.safe_feedrate;
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
|
||||
// calculates block entry feedrate
|
||||
float vmax_junction = curr.safe_feedrate;
|
||||
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
|
||||
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
|
||||
if (!use_junction_deviation && has_prev_move) {
|
||||
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
|
||||
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
|
||||
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
|
||||
@@ -5471,6 +5486,10 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
|
||||
if (!is_extrusion_only_move(delta_pos))
|
||||
curr.enter_direction = curr.enter_direction / norm;
|
||||
curr.exit_direction = curr.enter_direction;
|
||||
curr.jd_unit_vec = Vec4f(static_cast<float>(delta_pos[X]),
|
||||
static_cast<float>(delta_pos[Y]),
|
||||
static_cast<float>(delta_pos[Z]),
|
||||
static_cast<float>(delta_pos[E])).normalized();
|
||||
|
||||
TimeBlock block;
|
||||
block.move_type = type;
|
||||
@@ -5551,22 +5570,32 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
|
||||
|
||||
block.acceleration = acceleration;
|
||||
|
||||
// calculates block exit feedrate
|
||||
curr.safe_feedrate = block.feedrate_profile.cruise;
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
||||
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
// Orca: junction deviation where the firmware uses it (Klipper always, Marlin 2 with M205 J).
|
||||
// Negative leaves the classic jerk path below unchanged.
|
||||
const float vmax_junction_jd = calc_vmax_junction_deviation(block, prev, curr, has_prev_move,
|
||||
static_cast<PrintEstimatedStatistics::ETimeMode>(i));
|
||||
const bool use_junction_deviation = vmax_junction_jd >= 0.0f;
|
||||
|
||||
// calculates block exit feedrate. Junction deviation has no per axis jerk floor, so a move is
|
||||
// free to start from rest.
|
||||
curr.safe_feedrate = use_junction_deviation ? 0.0f : block.feedrate_profile.cruise;
|
||||
|
||||
if (!use_junction_deviation) {
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
|
||||
if (curr.abs_axis_feedrate[a] > axis_max_jerk)
|
||||
curr.safe_feedrate = std::min(curr.safe_feedrate, axis_max_jerk);
|
||||
}
|
||||
}
|
||||
|
||||
block.feedrate_profile.exit = curr.safe_feedrate;
|
||||
|
||||
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
||||
|
||||
// calculates block entry feedrate
|
||||
float vmax_junction = curr.safe_feedrate;
|
||||
if (!blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD) {
|
||||
float vmax_junction = use_junction_deviation ? vmax_junction_jd : curr.safe_feedrate;
|
||||
if (!use_junction_deviation && has_prev_move) {
|
||||
bool prev_speed_larger = prev.feedrate > block.feedrate_profile.cruise;
|
||||
float smaller_speed_factor = prev_speed_larger ? (block.feedrate_profile.cruise / prev.feedrate) : (prev.feedrate / block.feedrate_profile.cruise);
|
||||
// Pick the smaller of the nominal speeds. Higher speed shall not be achieved at the junction during coasting.
|
||||
@@ -5997,8 +6026,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)
|
||||
@@ -6007,8 +6039,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)
|
||||
@@ -7256,6 +7291,91 @@ float GCodeProcessor::get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeM
|
||||
return get_axis_max_jerk_with_jd(mode, axis, get_acceleration(mode));
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const
|
||||
{
|
||||
const size_t id = static_cast<size_t>(mode);
|
||||
|
||||
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
|
||||
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
|
||||
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
|
||||
if (m_flavor == gcfKlipper) {
|
||||
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
|
||||
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
|
||||
if (scv <= 0.0f || acceleration <= 0.0f)
|
||||
return 0.0f;
|
||||
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
|
||||
}
|
||||
|
||||
// Marlin 2 plans with junction deviation only when M205 J > 0; classic jerk leaves it at 0.
|
||||
if (m_flavor == gcfMarlinFirmware)
|
||||
return get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, id);
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float GCodeProcessor::calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
float junction_acceleration = block.acceleration;
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
if (junction_unit_vec[a] == 0.0f)
|
||||
continue;
|
||||
const float axis_max_acceleration = get_axis_max_acceleration(mode, static_cast<Axis>(a), m_machine_config_idx);
|
||||
if (axis_max_acceleration > 0.0f)
|
||||
junction_acceleration = std::min(junction_acceleration, std::abs(axis_max_acceleration / junction_unit_vec[a]));
|
||||
}
|
||||
return junction_acceleration;
|
||||
}
|
||||
|
||||
// Ported from PrusaSlicer (src/libslic3r/GCode/GCodeProcessor.cpp).
|
||||
float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
|
||||
const TimeMachine::State& curr, bool has_prev_move,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const
|
||||
{
|
||||
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
|
||||
if (junction_deviation <= 0.0f)
|
||||
return -1.0f; // classic jerk machine, the caller keeps its own computation
|
||||
if (!has_prev_move)
|
||||
return 0.0f; // starts from rest, the planner raises this on the reverse pass
|
||||
|
||||
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
|
||||
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
|
||||
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
|
||||
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
|
||||
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
|
||||
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
|
||||
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
|
||||
if (junction_cos_theta > 0.999999f)
|
||||
return 0.0f; // the path doubles back, the machine has to stop
|
||||
junction_cos_theta = std::max(junction_cos_theta, -0.999999f); // guards the division below
|
||||
|
||||
const float sin_theta_d2 = std::sqrt(0.5f * (1.0f - junction_cos_theta)); // always positive
|
||||
const Vec4f junction_vec = curr.jd_unit_vec - prev.jd_unit_vec;
|
||||
const float junction_vec_norm = junction_vec.norm();
|
||||
const Vec4f junction_unit_vec = (junction_vec_norm > 0.0f) ? Vec4f(junction_vec / junction_vec_norm)
|
||||
: Vec4f(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
const float junction_acceleration = calc_junction_acceleration(block, junction_unit_vec, mode);
|
||||
|
||||
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
|
||||
|
||||
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
|
||||
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
|
||||
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
||||
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
|
||||
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
|
||||
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
|
||||
const float t = neg * junction_cos_theta;
|
||||
const float asinx = 0.032843707f + t * (-1.451838349f + t * (29.66153956f + t * (-131.1123477f +
|
||||
t * (262.8130562f + t * (-242.7199627f + t * (84.31466202f))))));
|
||||
const float junction_theta = float(0.5 * M_PI) + neg * asinx; // acos(-t), bottoms out at 0.033
|
||||
vmax_junction_sqr = std::min(vmax_junction_sqr, (block.distance * junction_acceleration) / junction_theta);
|
||||
}
|
||||
|
||||
// Never faster than either of the two moves the junction joins.
|
||||
vmax_junction_sqr = std::min(vmax_junction_sqr, std::min(sqr(block.feedrate_profile.cruise), sqr(prev.feedrate)));
|
||||
return std::sqrt(vmax_junction_sqr);
|
||||
}
|
||||
|
||||
float GCodeProcessor::get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const
|
||||
{
|
||||
const size_t id = static_cast<size_t>(mode);
|
||||
|
||||
@@ -660,6 +660,9 @@ class Print;
|
||||
//For line move, there are same. For arc move, there are different.
|
||||
Vec3f enter_direction;
|
||||
Vec3f exit_direction;
|
||||
// Orca: move direction over all four axes, unit length. Used by
|
||||
// calc_vmax_junction_deviation(); see there for why E is normalized in.
|
||||
Vec4f jd_unit_vec;
|
||||
|
||||
void reset();
|
||||
};
|
||||
@@ -1517,6 +1520,16 @@ class Print;
|
||||
float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const;
|
||||
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis, float acceleration) const;
|
||||
float get_axis_max_jerk_with_jd(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
|
||||
// Orca: junction deviation for a block at the given acceleration, 0 for a classic jerk machine.
|
||||
float get_junction_deviation(PrintEstimatedStatistics::ETimeMode mode, float acceleration) const;
|
||||
// Orca: acceleration along the junction direction, clamped by the per axis limits.
|
||||
float calc_junction_acceleration(const TimeBlock& block, const Vec4f& junction_unit_vec,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
// Orca: entry speed from the junction deviation model, which limits a corner by its angle alone
|
||||
// and is therefore isotropic, unlike per axis jerk. Negative means classic jerk applies instead.
|
||||
float calc_vmax_junction_deviation(const TimeBlock& block, const TimeMachine::State& prev,
|
||||
const TimeMachine::State& curr, bool has_prev_move,
|
||||
PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
|
||||
Vec3f get_xyz_max_jerk(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
float get_retract_acceleration(PrintEstimatedStatistics::ETimeMode mode) const;
|
||||
|
||||
@@ -32,7 +32,7 @@ using ThumbnailsList = std::vector<ThumbnailData>;
|
||||
|
||||
struct ThumbnailsParams
|
||||
{
|
||||
const Vec2ds sizes;
|
||||
const Vec2ds sizes{};
|
||||
bool printable_only;
|
||||
bool parts_only;
|
||||
bool show_bed;
|
||||
|
||||
@@ -617,6 +617,18 @@ Polygon generate_rectange_polygon(const Vec2f &wt_box_min ,const Vec2f & wt_box_
|
||||
return res;
|
||||
}
|
||||
|
||||
const char* flush_planner_queue_command(GCodeFlavor flavor)
|
||||
{
|
||||
return flavor == gcfKlipper ? "M400\n" : "G4 S0\n";
|
||||
}
|
||||
|
||||
std::string wait_command(GCodeFlavor flavor, float seconds)
|
||||
{
|
||||
if (flavor == gcfKlipper)
|
||||
return "G4 P" + std::to_string(std::lround(seconds * 1000.f)) + "\n";
|
||||
return "G4 S" + Slic3r::float_to_string_decimal_point(seconds, 3) + "\n";
|
||||
}
|
||||
|
||||
class WipeTowerWriter
|
||||
{
|
||||
public:
|
||||
@@ -1145,7 +1157,7 @@ public:
|
||||
{
|
||||
if (time==0.f)
|
||||
return *this;
|
||||
m_gcode += "G4 S" + Slic3r::float_to_string_decimal_point(time, 3) + "\n";
|
||||
m_gcode += wait_command(m_gcode_flavor, time);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -1190,7 +1202,7 @@ public:
|
||||
|
||||
WipeTowerWriter& flush_planner_queue()
|
||||
{
|
||||
m_gcode += "G4 S0\n";
|
||||
m_gcode += flush_planner_queue_command(m_gcode_flavor);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -1333,6 +1345,8 @@ public:
|
||||
{
|
||||
std::string buffer;
|
||||
if (wait_for_moves)
|
||||
// Not flush_planner_queue_command(): this BBL precool path wants M400, which every
|
||||
// flavor it reaches understands, not the zero dwell the other flavors flush with.
|
||||
buffer += "M400\n";
|
||||
buffer += "M104";
|
||||
if (target_extruder != -1)
|
||||
@@ -1616,25 +1630,56 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
Vec2f WipeTower::move_box_inside_box(const BoundingBox &box1, const BoundingBox &box2,int scaled_offset)
|
||||
Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset)
|
||||
{
|
||||
Vec2f res{0, 0};
|
||||
if (box1.size()[0] >= box2.size()[0]- 2*scaled_offset || box1.size()[1] >= box2.size()[1]-2*scaled_offset) return res;
|
||||
if (polygons.empty()) return Vec2f{0.f, 0.f};
|
||||
|
||||
if (box1.max[0] > box2.max[0] - scaled_offset) {
|
||||
res[0] = unscaled<float>((box2.max[0] - scaled_offset) - box1.max[0]);
|
||||
}
|
||||
else if (box1.min[0] < box2.min[0] + scaled_offset) {
|
||||
res[0] = unscaled<float>((box2.min[0] + scaled_offset) - box1.min[0]);
|
||||
const BoundingBox bed = get_extents(polygons);
|
||||
// No position fits the footprint.
|
||||
if (box.size().x() >= bed.size().x() - 2 * offset || box.size().y() >= bed.size().y() - 2 * offset)
|
||||
return Vec2f{0.f, 0.f};
|
||||
|
||||
// Clamp against the bounding box first, moving only along the axis that is violated so a dragged
|
||||
// prime tower slides along the bed edge instead of jumping inwards.
|
||||
Point shift(0, 0);
|
||||
for (int axis = 0; axis < 2; ++axis) {
|
||||
if (box.max[axis] > bed.max[axis] - offset)
|
||||
shift[axis] = (bed.max[axis] - offset) - box.max[axis];
|
||||
else if (box.min[axis] < bed.min[axis] + offset)
|
||||
shift[axis] = (bed.min[axis] + offset) - box.min[axis];
|
||||
}
|
||||
|
||||
if (box1.max[1] > box2.max[1] - scaled_offset) {
|
||||
res[1] = unscaled<float>((box2.max[1] - scaled_offset) - box1.max[1]);
|
||||
// A bed that fills its own bounding box is fully clamped by that, so every rectangular bed — all
|
||||
// but the delta-style profiles — stops here and keeps its historic placement, including when a
|
||||
// negative margin lets the footprint hang over the edge. The tolerance is relative because an
|
||||
// exact rectangle loses a few ulps once the areas are squared world coordinates.
|
||||
double area = 0.;
|
||||
for (const Polygon &poly : polygons) area += std::abs(poly.area());
|
||||
const double bed_area = double(bed.size().x()) * double(bed.size().y());
|
||||
if (area >= bed_area * (1. - EPSILON)) return unscaled<float>(shift);
|
||||
|
||||
// Clamp a negative margin (an auto brim width that has not been resolved yet) to zero: padding by
|
||||
// it would shrink the footprint and hand back a position the validation still rejects. The
|
||||
// epsilon lets the move's round trip through millimeters land on the outline without counting as
|
||||
// a violation.
|
||||
BoundingBox padded = box.inflated(std::max<coord_t>(offset, 0) - SCALED_EPSILON);
|
||||
padded.translate(shift);
|
||||
auto fits = [&padded, &polygons](const Point &move) {
|
||||
BoundingBox moved = padded;
|
||||
moved.translate(move);
|
||||
return diff(Polygons{moved.polygon()}, polygons).empty();
|
||||
};
|
||||
if (fits(Point(0, 0))) return unscaled<float>(shift);
|
||||
|
||||
// Walk towards the middle of the bed. On every non-rectangular bed we ship, the fitting positions
|
||||
// form a convex region around it, so bisecting stops just inside the outline.
|
||||
Point lo(0, 0), hi = bed.center() - padded.center();
|
||||
if (!fits(hi)) return unscaled<float>(shift);
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
const Point mid = (lo + hi) / 2;
|
||||
if (fits(mid)) hi = mid; else lo = mid;
|
||||
}
|
||||
else if (box1.min[1] < box2.min[1] + scaled_offset) {
|
||||
res[1] = unscaled<float>((box2.min[1] + scaled_offset) - box1.min[1]);
|
||||
}
|
||||
return res;
|
||||
return unscaled<float>(Point(shift + hi));
|
||||
}
|
||||
|
||||
Polygon WipeTower::rib_section(float width, float depth, float rib_length, float rib_width,bool fillet_wall)
|
||||
|
||||
@@ -26,6 +26,12 @@ enum GCodeFlavor : unsigned char;
|
||||
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
|
||||
{
|
||||
public:
|
||||
@@ -39,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
|
||||
{
|
||||
|
||||
@@ -386,7 +386,8 @@ public:
|
||||
}
|
||||
|
||||
WipeTowerWriter2& switch_filament_monitoring(bool enable) {
|
||||
m_gcode += std::string("G4 S0\n") + "M591 " + (enable ? "R" : "S0") + "\n";
|
||||
flush_planner_queue();
|
||||
m_gcode += enable ? "M591 R\n" : "M591 S0\n";
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -412,6 +413,7 @@ public:
|
||||
const Vec2f& pos() const { return m_current_pos; }
|
||||
const Vec2f start_pos_rotated() const { return m_start_pos; }
|
||||
const Vec2f pos_rotated() const { return this->rotate(m_current_pos); }
|
||||
const Vec2f rotated(const Vec2f &pt) const { return this->rotate(pt); }
|
||||
float elapsed_time() const { return m_elapsed_time; }
|
||||
float get_and_reset_used_filament_length() { float temp = m_used_filament_length; m_used_filament_length = 0.f; return temp; }
|
||||
|
||||
@@ -623,10 +625,13 @@ public:
|
||||
}
|
||||
|
||||
// Set extruder temperature, don't wait by default.
|
||||
WipeTowerWriter2& set_extruder_temp(int temperature, bool wait = false)
|
||||
WipeTowerWriter2& set_extruder_temp(int temperature, bool wait = false, const std::string& comment = std::string())
|
||||
{
|
||||
m_gcode += "G4 S0\n"; // to flush planner queue
|
||||
m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature) + "\n";
|
||||
flush_planner_queue();
|
||||
m_gcode += "M" + std::to_string(wait ? 109 : 104) + " S" + std::to_string(temperature);
|
||||
if (!comment.empty())
|
||||
m_gcode += " " + comment;
|
||||
m_gcode += "\n";
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -635,7 +640,7 @@ public:
|
||||
{
|
||||
if (time==0.f)
|
||||
return *this;
|
||||
m_gcode += "G4 S" + Slic3r::float_to_string_decimal_point(time, 3) + "\n";
|
||||
m_gcode += wait_command(m_gcode_flavor, time);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -677,8 +682,8 @@ public:
|
||||
}
|
||||
|
||||
WipeTowerWriter2& flush_planner_queue()
|
||||
{
|
||||
m_gcode += "G4 S0\n";
|
||||
{
|
||||
m_gcode += flush_planner_queue_command(m_gcode_flavor);
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -1005,6 +1010,13 @@ bool WipeTower2::use_gap_wall(const PrintConfig& config)
|
||||
return config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone;
|
||||
}
|
||||
|
||||
bool WipeTower2::wait_for_temp_enabled(const PrintConfig& config)
|
||||
{
|
||||
// SEMM runs its own unload/load temperature sequence; the GUI hides the option
|
||||
// there but a profile may still carry it set.
|
||||
return config.wait_for_temp_on_wipe_tower.value && !config.single_extruder_multi_material.value;
|
||||
}
|
||||
|
||||
WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& default_region_config,int plate_idx, Vec3d plate_origin, const std::vector<std::vector<float>>& wiping_matrix, size_t initial_tool) :
|
||||
m_semm(config.single_extruder_multi_material.value),
|
||||
m_enable_filament_ramming(config.enable_filament_ramming.value),
|
||||
@@ -1034,7 +1046,8 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
|
||||
m_wall_type((int)config.wipe_tower_wall_type),
|
||||
m_use_gap_wall(use_gap_wall(config)),
|
||||
m_enable_tower_interface_features(config.enable_tower_interface_features.value),
|
||||
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value)
|
||||
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value),
|
||||
m_wait_for_temp_on_wipe_tower(wait_for_temp_enabled(config))
|
||||
{
|
||||
// Read absolute value of first layer speed, if given as percentage,
|
||||
// it is taken over following default. Speeds from config are not
|
||||
@@ -1084,6 +1097,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
|
||||
m_bed_bottom_left = m_bed_shape == RectangularBed
|
||||
? Vec2f(bed_points.front().x(), bed_points.front().y())
|
||||
: Vec2f::Zero();
|
||||
m_bed_polygon = Polygon::new_scale(bed_points);
|
||||
}
|
||||
|
||||
|
||||
@@ -1235,7 +1249,7 @@ std::vector<WipeTower::ToolChangeResult> WipeTower2::prime(
|
||||
|
||||
unsigned int tool = tools[idx_tool];
|
||||
m_left_to_right = true;
|
||||
toolchange_Change(writer, tool, m_filpar[tool].material); // Select the tool, set a speed override for soluble and flex materials.
|
||||
toolchange_Change(writer, tool, m_filpar[tool].material, m_filpar[tool].first_layer_temperature, false); // Select the tool, set a speed override for soluble and flex materials.
|
||||
toolchange_Load(writer, cleaning_box); // Prime the tool.
|
||||
if (idx_tool + 1 == tools.size()) {
|
||||
// Last tool should not be unloaded, but it should be wiped enough to become of a pure color.
|
||||
@@ -1354,13 +1368,19 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool)
|
||||
toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material,
|
||||
(is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature),
|
||||
new_tool_temp);
|
||||
toolchange_Change(writer, tool, m_filpar[tool].material); // Change the tool, set a speed override for soluble and flex materials.
|
||||
// Wait-at-tower target: the interface temp when an interface boost applies on this layer,
|
||||
// otherwise the print temp (nozzle_temperature == 0 means "use the first layer temp").
|
||||
int wait_for_temp = interface_layer && m_filpar[tool].interface_print_temperature > 0 ?
|
||||
m_filpar[tool].interface_print_temperature :
|
||||
(is_first_layer() || m_filpar[tool].temperature == 0 ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature);
|
||||
toolchange_Change(writer, tool, m_filpar[tool].material, wait_for_temp, true); // Change the tool, set a speed override for soluble and flex materials.
|
||||
toolchange_Load(writer, cleaning_box);
|
||||
writer.travel(writer.x(), writer.y()-m_perimeter_width); // cooling and loading were done a bit down the road
|
||||
int base_temp = is_first_layer() ? m_filpar[tool].first_layer_temperature : m_filpar[tool].temperature;
|
||||
if (interface_layer) {
|
||||
int interface_temp = m_filpar[tool].interface_print_temperature;
|
||||
if (interface_temp > 0 && interface_temp != base_temp)
|
||||
// With wait-for-temp-on-wipe-tower the toolchange already blocked for the interface temp.
|
||||
if (interface_temp > 0 && interface_temp != base_temp && !m_wait_for_temp_on_wipe_tower)
|
||||
writer.set_extruder_temp(interface_temp, true);
|
||||
if (m_enable_tower_interface_cooldown_during_tower && interface_temp > 0 && interface_temp != base_temp)
|
||||
writer.set_extruder_temp(base_temp, false);
|
||||
@@ -1670,7 +1690,9 @@ void WipeTower2::toolchange_Unload(
|
||||
void WipeTower2::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)
|
||||
{
|
||||
// Ask the writer about how much of the old filament we consumed:
|
||||
if (m_current_tool < m_used_filament_length.size())
|
||||
@@ -1684,6 +1706,90 @@ void WipeTower2::toolchange_Change(
|
||||
if (m_is_mk4mmu3)
|
||||
writer.switch_filament_monitoring(true);
|
||||
|
||||
const bool wait_for_temp_here = m_wait_for_temp_on_wipe_tower && wait_for_temp > 0;
|
||||
|
||||
// The Tn above was issued without a blocking temperature wait (GCode::set_extruder only raises
|
||||
// the target, ahead of the Tn); block here, before the deretraction below, which must not
|
||||
// extrude on a cold nozzle. Like the Bambu H2C, park beside the tower for the heat-up so drool lands
|
||||
// next to it instead of on its top surface — nearest x side first, then that side clamped
|
||||
// toward the bed edge, then the far side, in place if all would leave the bed. Raw
|
||||
// pre-rotated moves (see the repositioning move below) keep the writer's tracked position
|
||||
// at the tower entry. The tag keeps the
|
||||
// interface-temp deduplication pass in append_tcr2 from stripping the M109.
|
||||
if (wait_for_temp_here && wait_beside_tower) {
|
||||
// The rib wall and the stabilization cone bulge past the nominal width rectangle
|
||||
// (widest near the bottom), and the first-layer brim is printed around the wall later
|
||||
// in the layer — clear the widest of them, not just the rectangle, so the park point
|
||||
// and its drool stay off the tower.
|
||||
float min_x = 0.f, max_x = m_wipe_tower_width;
|
||||
if (m_wall_type == (int)wtwRib) {
|
||||
WipeTower::box_coordinates wt_box(Vec2f(0.f, 0.f), m_wipe_tower_width, m_layer_info->depth + m_perimeter_width);
|
||||
const BoundingBox rib_bbox = get_extents(generate_rib_polygon(wt_box)); // the fillet stays within this bbox
|
||||
min_x = std::min(min_x, unscaled<float>(rib_bbox.min.x()));
|
||||
max_x = std::max(max_x, unscaled<float>(rib_bbox.max.x()));
|
||||
} else if (m_wall_type == (int)wtwCone) {
|
||||
const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
|
||||
m_wipe_tower_cone_angle).second;
|
||||
const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z;
|
||||
const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z);
|
||||
const double w = m_layer_info->depth + m_perimeter_width;
|
||||
if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall
|
||||
const float bulge = float(std::sqrt(r * r - 0.25 * w * w) / support_scale);
|
||||
min_x = std::min(min_x, m_wipe_tower_width / 2.f - bulge);
|
||||
max_x = std::max(max_x, m_wipe_tower_width / 2.f + bulge);
|
||||
}
|
||||
}
|
||||
if (is_first_layer()) {
|
||||
const float brim = m_wipe_tower_brim_width < 0.f ? WipeTower::get_auto_brim_by_height(m_wipe_tower_height) :
|
||||
m_wipe_tower_brim_width;
|
||||
min_x -= brim;
|
||||
max_x += brim;
|
||||
}
|
||||
constexpr float gap = 2.f;
|
||||
constexpr float min_gap = 0.5f;
|
||||
const bool on_left = writer.x() < m_wipe_tower_width / 2.f;
|
||||
const float near_x = on_left ? min_x - gap : max_x + gap;
|
||||
const float far_x = on_left ? max_x + gap : min_x - gap;
|
||||
const Eigen::Rotation2Df to_bed(float(Geometry::deg2rad(m_wipe_tower_rotation_angle)));
|
||||
auto park_pt_on_bed = [this, &writer, to_bed](float side_x) {
|
||||
const Vec2f bed_pt = to_bed * (writer.rotated(Vec2f(side_x, writer.y())) + m_rib_offset) + m_wipe_tower_pos;
|
||||
return m_bed_polygon.contains(Point::new_scale(bed_pt.x(), bed_pt.y()));
|
||||
};
|
||||
float park_x = near_x;
|
||||
bool have_park = park_pt_on_bed(near_x);
|
||||
if (!have_park) {
|
||||
// The ideal near point hangs off the bed: pull it back to the bed edge as long
|
||||
// as that still clears the tower envelope by min_gap (the BBL tower clamps its
|
||||
// stop_pos against the bed the same way in append_tcr). Bisection, because with
|
||||
// tower rotation and non-rectangular beds the bed edge is not axis-aligned.
|
||||
const float limit_x = on_left ? min_x - min_gap : max_x + min_gap;
|
||||
if (park_pt_on_bed(limit_x)) {
|
||||
float on = limit_x, off = near_x;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const float mid = 0.5f * (on + off);
|
||||
if (park_pt_on_bed(mid))
|
||||
on = mid;
|
||||
else
|
||||
off = mid;
|
||||
}
|
||||
park_x = on;
|
||||
have_park = true;
|
||||
}
|
||||
}
|
||||
if (!have_park && park_pt_on_bed(far_x)) {
|
||||
park_x = far_x;
|
||||
have_park = true;
|
||||
}
|
||||
if (have_park) {
|
||||
const Vec2f stop = writer.rotated(Vec2f(park_x, writer.y()));
|
||||
writer.feedrate(m_travel_speed * 60.f)
|
||||
.append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(stop.x())
|
||||
+ " Y" + Slic3r::float_to_string_decimal_point(stop.y())
|
||||
+ never_skip_tag() + "\n");
|
||||
}
|
||||
writer.set_extruder_temp(wait_for_temp, true, wait_for_temp_tag());
|
||||
}
|
||||
|
||||
// Travel to where we assume we are. Custom toolchange or some special T code handling (parking extruder etc)
|
||||
// gcode could have left the extruder somewhere, we cannot just start extruding. We should also inform the
|
||||
// postprocessor that we absolutely want to have this in the gcode, even if it thought it is the same as before.
|
||||
@@ -1694,6 +1800,10 @@ void WipeTower2::toolchange_Change(
|
||||
+ never_skip_tag() + "\n"
|
||||
);
|
||||
|
||||
// Priming has no tower to park beside — wait right at the priming line instead.
|
||||
if (wait_for_temp_here && !wait_beside_tower)
|
||||
writer.set_extruder_temp(wait_for_temp, true, wait_for_temp_tag());
|
||||
|
||||
writer.append("[deretraction_from_wipe_tower_generator]");
|
||||
|
||||
// The toolchange Tn command will be inserted later, only in case that the user does
|
||||
@@ -2020,31 +2130,68 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
|
||||
}
|
||||
|
||||
// Static method to extract wipe_volumes[from][to] from the configuration.
|
||||
std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const PrintConfig& config)
|
||||
// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
|
||||
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
|
||||
std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const ConfigBase& config)
|
||||
{
|
||||
// Get wiping matrix to get number of extruders and convert vector<double> to vector<float>:
|
||||
std::vector<float> wiping_matrix(cast<float>(config.flush_volumes_matrix.values));
|
||||
auto scale = config.flush_multiplier.get_at(0);
|
||||
// flush_volumes_matrix holds one filaments x filaments block per nozzle (written by
|
||||
// PresetBundle::update_multi_material_filament_presets), so the filament count is
|
||||
// sqrt(size / nozzles). One tower serves every nozzle and the filament to nozzle assignment is
|
||||
// only decided later by ToolOrdering, so fold the blocks with std::max: the depth reserved here
|
||||
// has to cover the worst nozzle. With a single nozzle the fold has one term.
|
||||
const std::vector<double> &raw_matrix = config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
|
||||
const auto *nozzle_diameter = config.option<ConfigOptionFloats>("nozzle_diameter");
|
||||
size_t nozzle_nums = (nozzle_diameter == nullptr || nozzle_diameter->values.empty()) ? 1 : nozzle_diameter->values.size();
|
||||
unsigned int number_of_extruders = (unsigned int)(sqrt(raw_matrix.size() / nozzle_nums) + EPSILON);
|
||||
if (size_t(number_of_extruders) * number_of_extruders * nozzle_nums != raw_matrix.size()) {
|
||||
// Saved for a different nozzle count (older project, or the printer was just switched):
|
||||
// fall back to reading the whole option as one block, as this did before.
|
||||
nozzle_nums = 1;
|
||||
number_of_extruders = (unsigned int)(sqrt(raw_matrix.size()) + EPSILON);
|
||||
}
|
||||
|
||||
// The values shall only be used when SEMM is enabled. The purging for other printers
|
||||
// is determined by filament_minimal_purge_on_wipe_tower.
|
||||
if (! config.purge_in_prime_tower.value || ! config.single_extruder_multi_material.value)
|
||||
std::fill(wiping_matrix.begin(), wiping_matrix.end(), 0.f);
|
||||
const bool purge = config.option<ConfigOptionBool>("purge_in_prime_tower")->value
|
||||
&& config.option<ConfigOptionBool>("single_extruder_multi_material")->value;
|
||||
|
||||
// Extract purging volumes for each extruder pair:
|
||||
std::vector<std::vector<float>> wipe_volumes;
|
||||
const unsigned int number_of_extruders = (unsigned int)(sqrt(wiping_matrix.size())+EPSILON);
|
||||
for (size_t i = 0; i<number_of_extruders; ++i)
|
||||
wipe_volumes.push_back(std::vector<float>(wiping_matrix.begin()+i*number_of_extruders, wiping_matrix.begin()+(i+1)*number_of_extruders));
|
||||
// Extract purging volumes for each extruder pair, each nozzle's block scaled by its own multiplier:
|
||||
std::vector<std::vector<float>> wipe_volumes(number_of_extruders, std::vector<float>(number_of_extruders, 0.f));
|
||||
if (purge) {
|
||||
const auto *multiplier = config.option<ConfigOptionFloats>("flush_multiplier");
|
||||
for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) {
|
||||
const std::vector<double> block = get_flush_volumes_matrix(raw_matrix, nozzle_id, nozzle_nums);
|
||||
const double scale = multiplier->get_at(nozzle_id);
|
||||
for (unsigned int i = 0; i<number_of_extruders; ++i)
|
||||
for (unsigned int j = 0; j<number_of_extruders; ++j)
|
||||
wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j], float(block[size_t(i) * number_of_extruders + j]) * scale);
|
||||
}
|
||||
}
|
||||
|
||||
// Also include filament_minimal_purge_on_wipe_tower. This is needed for the preview.
|
||||
const auto *minimal_purge = config.option<ConfigOptionFloats>("filament_minimal_purge_on_wipe_tower");
|
||||
for (unsigned int i = 0; i<number_of_extruders; ++i)
|
||||
for (unsigned int j = 0; j<number_of_extruders; ++j)
|
||||
wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j] * scale, config.filament_minimal_purge_on_wipe_tower.get_at(j));
|
||||
wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j], minimal_purge->get_at(j));
|
||||
|
||||
return wipe_volumes;
|
||||
}
|
||||
|
||||
float WipeTower2::estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt)
|
||||
{
|
||||
const std::vector<std::vector<float>> wipe_volumes = extract_wipe_volumes(config);
|
||||
if (wipe_volumes.empty()) // an empty flush matrix would make the average below 0/0
|
||||
return 0.f;
|
||||
float maximum = 0.f;
|
||||
for (const std::vector<float> &v : wipe_volumes)
|
||||
maximum += *std::max_element(v.begin(), v.end());
|
||||
maximum = maximum * filaments_cnt / wipe_volumes.size();
|
||||
|
||||
// Orca: it's overshooting a bit, so let's reduce it a bit
|
||||
maximum *= 0.6;
|
||||
return maximum;
|
||||
}
|
||||
|
||||
static float get_wipe_depth(float volume, float layer_height, float perimeter_width, float extra_flow, float extra_spacing, float width)
|
||||
{
|
||||
float length_to_extrude = (volume_to_length(volume, perimeter_width, layer_height)) / extra_flow;
|
||||
|
||||
@@ -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.
|
||||
@@ -38,6 +45,11 @@ public:
|
||||
// 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 )
|
||||
@@ -227,6 +239,7 @@ private:
|
||||
size_t m_first_layer_idx = size_t(-1);
|
||||
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;
|
||||
|
||||
@@ -263,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.
|
||||
@@ -385,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,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "I18N.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Geometry/ArcWelder.hpp"
|
||||
#include "Line.hpp"
|
||||
#include <algorithm>
|
||||
#include <iomanip>
|
||||
@@ -1086,45 +1087,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");
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -3243,9 +3243,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.;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include "OpenVDBUtils.hpp"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
|
||||
+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;
|
||||
};
|
||||
|
||||
@@ -229,6 +229,22 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
|
||||
|
||||
// Append thin walls to the nearest-neighbor search (only for first iteration)
|
||||
if (! thin_walls.empty()) {
|
||||
// Orca: apply fuzzy skin to thin walls as well
|
||||
for (auto& thin_wall : thin_walls) {
|
||||
// First, we convert the ThickPolyline into Arachne::ExtrusionLine so we could reuse our existing fuzzy code
|
||||
Arachne::ExtrusionLine el(0, true);
|
||||
el.junctions.reserve(thin_wall.points.size());
|
||||
for (int i = 0; i < thin_wall.points.size(); i++) {
|
||||
el.junctions.emplace_back(thin_wall.points[i], thin_wall.width[i], 0);
|
||||
}
|
||||
|
||||
// Then we fuzzy it
|
||||
apply_fuzzy_skin(&el, perimeter_generator, true, thin_wall.is_closed());
|
||||
|
||||
// Then convert the result back to ThickPolyline
|
||||
thin_wall = Arachne::to_thick_polyline(el);
|
||||
}
|
||||
|
||||
variable_width(thin_walls, erExternalPerimeter, perimeter_generator.ext_perimeter_flow, coll.entities);
|
||||
thin_walls.clear();
|
||||
}
|
||||
@@ -1977,7 +1993,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());
|
||||
@@ -2090,35 +2106,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
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
+36
-16
@@ -8,7 +8,9 @@
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif /* _MSC_VER */
|
||||
|
||||
@@ -147,6 +149,9 @@ Semver get_version_from_json(std::string file_path)
|
||||
return Semver();
|
||||
//throw ConfigurationError(format("Failed loading configuration file \"%1%\": %2%", file_path, err.what()));
|
||||
}
|
||||
catch(...) {
|
||||
return Semver();
|
||||
}
|
||||
}
|
||||
|
||||
//BBS: add a function to load the key-values from xxx.json
|
||||
@@ -261,18 +266,28 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
|
||||
}
|
||||
};
|
||||
|
||||
// The four variant sets are immutable after static init and probed for every
|
||||
// key of every preset loaded; one merged map makes that a single lookup.
|
||||
// emplace keeps the first insertion, preserving the first-set-wins priority
|
||||
// of the else-if chain this replaces.
|
||||
static const std::unordered_map<std::string, int> variant_class = [] {
|
||||
std::unordered_map<std::string, int> m;
|
||||
for (const std::string& k : print_options_with_variant) m.emplace(k, 0);
|
||||
for (const std::string& k : filament_options_with_variant) m.emplace(k, 1);
|
||||
for (const std::string& k : printer_options_with_variant_1) m.emplace(k, 2);
|
||||
for (const std::string& k : printer_options_with_variant_2) m.emplace(k, 3);
|
||||
return m;
|
||||
}();
|
||||
|
||||
for(auto& key :config.keys()){
|
||||
if(auto iter = print_options_with_variant.find(key); iter != print_options_with_variant.end()){
|
||||
replace_nil_and_resize(key, process_variant_length);
|
||||
}
|
||||
else if(auto iter = filament_options_with_variant.find(key); iter != filament_options_with_variant.end()){
|
||||
replace_nil_and_resize(key, filament_variant_length);
|
||||
}
|
||||
else if(auto iter = printer_options_with_variant_1.find(key); iter != printer_options_with_variant_1.end()){
|
||||
replace_nil_and_resize(key, machine_variant_length);
|
||||
}
|
||||
else if(auto iter = printer_options_with_variant_2.find(key); iter != printer_options_with_variant_2.end()){
|
||||
replace_nil_and_resize(key, machine_variant_length * 2);
|
||||
auto iter = variant_class.find(key);
|
||||
if (iter == variant_class.end())
|
||||
continue;
|
||||
switch (iter->second) {
|
||||
case 0: replace_nil_and_resize(key, process_variant_length); break;
|
||||
case 1: replace_nil_and_resize(key, filament_variant_length); break;
|
||||
case 2: replace_nil_and_resize(key, machine_variant_length); break;
|
||||
case 3: replace_nil_and_resize(key, machine_variant_length * 2); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1037,6 +1052,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 +1106,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", "leading_brim_length", "extra_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", "leading_brim_length", "extra_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
|
||||
@@ -1352,6 +1368,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",
|
||||
@@ -1413,7 +1431,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness",
|
||||
"belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode",
|
||||
"enable_belt_purge_tower",
|
||||
"gcode_flavor",
|
||||
"gcode_flavor", "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",
|
||||
@@ -1431,7 +1449,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",
|
||||
@@ -3794,12 +3812,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);
|
||||
|
||||
@@ -131,6 +131,10 @@ public:
|
||||
PrinterVariant() {}
|
||||
PrinterVariant(const std::string &name) : name(name) {}
|
||||
std::string name;
|
||||
|
||||
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) { ar(name); } // PrinterVariant
|
||||
};
|
||||
|
||||
struct PrinterModel {
|
||||
@@ -139,7 +143,7 @@ public:
|
||||
std::string name;
|
||||
//BBS: this is internal id for the printer. Currently only used for searching in database
|
||||
std::string model_id;
|
||||
PrinterTechnology technology;
|
||||
PrinterTechnology technology = ptFFF;
|
||||
std::string family;
|
||||
std::vector<PrinterVariant> variants;
|
||||
std::vector<std::string> default_materials;
|
||||
@@ -162,6 +166,17 @@ public:
|
||||
}
|
||||
|
||||
const PrinterVariant* variant(const std::string &name) const { return const_cast<PrinterModel*>(this)->variant(name); }
|
||||
|
||||
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) // PrinterModel
|
||||
{
|
||||
ar(id, name, model_id, technology, family, variants, default_materials,
|
||||
not_support_bed_types, bed_model, bed_texture, image_bed_type,
|
||||
bottom_texture_end_name, use_double_extruder_default_texture,
|
||||
bottom_texture_rect, bottom_texture_rect_longer, middle_texture_rect,
|
||||
hotend_model);
|
||||
}
|
||||
};
|
||||
std::vector<PrinterModel> models;
|
||||
|
||||
@@ -173,6 +188,14 @@ public:
|
||||
|
||||
bool valid() const { return ! name.empty() && ! id.empty() && config_version.valid(); }
|
||||
|
||||
// All fields, declaration order — keep in sync; bump CACHE_VERSION on change.
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) // VendorProfile
|
||||
{
|
||||
ar(name, id, config_version, config_update_url, changelog_url,
|
||||
models, default_filaments, default_sla_materials);
|
||||
}
|
||||
|
||||
// Load VendorProfile from an ini file.
|
||||
// If `load_all` is false, only the header with basic info (name, version, URLs) is loaded.
|
||||
static VendorProfile from_ini(const boost::filesystem::path &path, bool load_all=true);
|
||||
@@ -427,10 +450,10 @@ public:
|
||||
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
||||
|
||||
protected:
|
||||
Preset() = default;
|
||||
|
||||
friend class PresetCollection;
|
||||
friend class PresetBundle;
|
||||
|
||||
Preset() = default;
|
||||
};
|
||||
|
||||
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);
|
||||
|
||||
+532
-342
File diff suppressed because it is too large
Load Diff
@@ -2,10 +2,12 @@
|
||||
#define slic3r_PresetBundle_hpp_
|
||||
|
||||
#include "Preset.hpp"
|
||||
#include "PresetCacheFormat.hpp"
|
||||
#include "AppConfig.hpp"
|
||||
#include "enum_bitmask.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <optional>
|
||||
@@ -170,6 +172,31 @@ struct PresetBundleMetadata
|
||||
class PresetBundle
|
||||
{
|
||||
public:
|
||||
// ---- Per-vendor preset cache --------------------------------------------
|
||||
// One cache file per vendor (plus the Orca filament library), stamped with
|
||||
// the vendor's own profile version rather than a directory scan. The bytes
|
||||
// on disk are VendorCacheFile's business (PresetCacheFormat.hpp); what
|
||||
// lives here is how a cache's contents install into a bundle.
|
||||
|
||||
// The cache is not something a caller loads from: a vendor is loaded with
|
||||
// load_vendor_configs_from_json, which comes from the cache whenever one covers
|
||||
// it. What is public here is what the cache's own tests drive directly.
|
||||
|
||||
// Load a per-vendor cache into this bundle by installing its entries, with
|
||||
// base_bundle's filament library as the inheritance base. Rejects (returns
|
||||
// false, with this bundle left clean) unless VendorCacheFile::load accepts
|
||||
// the file — see its contract for the version and identity checks — and
|
||||
// every entry installs. Options this build no longer defines are dropped,
|
||||
// not fatal — the payload names its own keys.
|
||||
bool load_vendor_cache(const std::string& cache_path, const std::string& expected_vendor_name,
|
||||
const Semver& expected_vendor_version, const PresetBundle* base_bundle = nullptr);
|
||||
|
||||
// Enable writing a per-vendor cache after a JSON parse (off by default). Cache
|
||||
// content is pure parse output, so the guard is policy, not correctness: only
|
||||
// the deliberate generators (load_system_presets_from_json, the cache build
|
||||
// tool) write files, not every incidental load a dialog performs.
|
||||
void set_generate_vendor_caches(bool enable) { m_generate_vendor_caches = enable; }
|
||||
|
||||
static DynamicPrintConfig construct_full_config(Preset &in_printer_preset,
|
||||
Preset &in_print_preset,
|
||||
const DynamicPrintConfig &project_config,
|
||||
@@ -444,8 +471,12 @@ public:
|
||||
/*std::pair<PresetsConfigSubstitutions, size_t> load_configbundle(
|
||||
const std::string &path, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
|
||||
//Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance
|
||||
// Orca: `dir` is where the vendor is looked for — its own directory, whether or
|
||||
// not the profile JSONs are still there. A whole-vendor load comes from the
|
||||
// vendor's preset cache whenever one covers the profile on disk, and is parsed
|
||||
// from the JSONs in `dir` only when none does. Nothing here reads resources.
|
||||
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
|
||||
const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
|
||||
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
|
||||
|
||||
// Export a config bundle file containing all the presets and the names of the active presets.
|
||||
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
|
||||
@@ -517,11 +548,49 @@ public:
|
||||
// Orca: for validation only.
|
||||
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
|
||||
|
||||
// Errors the last load recorded. What the cache's error accounting promises —
|
||||
// a cache-served vendor reports what its parse would — is pinned against this.
|
||||
int error_count() const { return m_errors; }
|
||||
|
||||
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
|
||||
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
|
||||
bool check_preset_references() const;
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
||||
|
||||
private:
|
||||
// Load one vendor from the preset cache installed in `dir`, judged against
|
||||
// the vendor profile there. False, with this bundle left clean, when there
|
||||
// is no usable cache and the vendor has to be parsed. This is how
|
||||
// load_vendor_configs_from_json reads a cache.
|
||||
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
|
||||
|
||||
// Load one source-form preset entry into this bundle: resolve `inherits`,
|
||||
// flatten, validate and register the preset. Returns the reason loading
|
||||
// failed, empty on success. See the definition for the sharing contract
|
||||
// between the JSON parse and the cache load.
|
||||
// retain_configs, when non-null, names the only presets registered into
|
||||
// config_maps (a full config copy each). The cache load passes the names its
|
||||
// entries inherit — the only ones ever looked up again; the JSON parse
|
||||
// retains all, not knowing what later subfiles inherit.
|
||||
std::string load_vendor_preset(const CachedPreset& entry,
|
||||
const std::string& path, const std::string& vendor_name,
|
||||
const PresetBundle* base_bundle,
|
||||
LoadConfigBundleAttributes flags,
|
||||
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
|
||||
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, std::string>& filament_id_maps,
|
||||
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
|
||||
const std::set<std::string>* retain_configs = nullptr);
|
||||
|
||||
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
||||
void clear_printer_hold_aliases();
|
||||
|
||||
// Whether to (re)write a per-vendor cache after a JSON parse.
|
||||
bool m_generate_vendor_caches { false };
|
||||
|
||||
// Orca: validation only - flag any printer with two or more compatible
|
||||
// filament presets sharing one filament_id (ambiguous AMS subtype match).
|
||||
bool check_duplicate_filament_subtypes() const;
|
||||
@@ -529,8 +598,6 @@ private:
|
||||
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
|
||||
//BBS: add json related logic
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
||||
// Update the multicolor information for filaments.
|
||||
void update_filament_multi_color();
|
||||
// Update renamed_from and alias maps of system profiles.
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
#include "libslic3r/PresetCacheFormat.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/crc.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/iostreams/device/array.hpp>
|
||||
#include <boost/iostreams/stream.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <cereal/types/map.hpp>
|
||||
#include <cereal/types/set.hpp>
|
||||
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
CacheDictionary::CacheDictionary()
|
||||
{
|
||||
// ENUM_UNNAMED is index 0 and always the empty name.
|
||||
m_enum_values.emplace_back();
|
||||
}
|
||||
|
||||
// The ints an enum option holds — one for a coEnum, the whole vector for coEnums.
|
||||
static std::vector<int> enum_ints(const ConfigOptionDef& def, const ConfigOption* opt)
|
||||
{
|
||||
if (def.type == coEnum)
|
||||
return { opt->getInt() };
|
||||
return static_cast<const ConfigOptionInts*>(opt)->values;
|
||||
}
|
||||
|
||||
// The name this build gives one of those ints, empty where it has none — a
|
||||
// nullable option's nil, or a definition carrying no enum_keys_map. Enums are
|
||||
// written by name so a build that reorders an enum's values still reads it right.
|
||||
static std::string enum_name_of(const ConfigOptionDef& def, int value)
|
||||
{
|
||||
if (def.enum_keys_map != nullptr)
|
||||
for (const auto& kvp : *def.enum_keys_map)
|
||||
if (kvp.second == value)
|
||||
return kvp.first;
|
||||
return {};
|
||||
}
|
||||
|
||||
void CacheDictionary::collect(const DynamicPrintConfig& config)
|
||||
{
|
||||
for (auto it = config.cbegin(); it != config.cend(); ++ it) {
|
||||
const ConfigOptionDef* def = print_config_def.get(it->first);
|
||||
if (def == nullptr)
|
||||
continue; // save_config does not write it either
|
||||
if (m_key_index.try_emplace(it->first, uint16_t(m_keys.size())).second) {
|
||||
m_keys.push_back(it->first);
|
||||
m_types.push_back(uint16_t(def->type));
|
||||
}
|
||||
if (def->type != coEnum && def->type != coEnums)
|
||||
continue;
|
||||
for (int value : enum_ints(*def, it->second.get())) {
|
||||
std::string name = enum_name_of(*def, value);
|
||||
if (! name.empty() && m_enum_index.try_emplace(name, uint16_t(m_enum_values.size())).second)
|
||||
m_enum_values.push_back(std::move(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t CacheDictionary::key_index(const t_config_option_key& key) const
|
||||
{
|
||||
auto it = m_key_index.find(key);
|
||||
if (it == m_key_index.end())
|
||||
throw std::runtime_error("preset cache: option " + key + " was never collected into the dictionary");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
uint16_t CacheDictionary::enum_index(const std::string& name) const
|
||||
{
|
||||
if (name.empty())
|
||||
return ENUM_UNNAMED;
|
||||
auto it = m_enum_index.find(name);
|
||||
return it == m_enum_index.end() ? ENUM_UNNAMED : it->second;
|
||||
}
|
||||
|
||||
void CacheDictionary::save(cereal::BinaryOutputArchive& ar) const
|
||||
{
|
||||
// Checked here rather than left to the caller: an index that wrapped would
|
||||
// be written silently, and nothing downstream could tell.
|
||||
if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES)
|
||||
throw std::runtime_error("preset cache: the option dictionary outgrew the uint16 it is indexed with");
|
||||
ar(m_keys, m_types, m_enum_values);
|
||||
}
|
||||
|
||||
void CacheDictionary::load(cereal::BinaryInputArchive& ar)
|
||||
{
|
||||
ar(m_keys, m_types, m_enum_values);
|
||||
if (m_keys.size() != m_types.size())
|
||||
throw std::runtime_error("preset cache: dictionary key and type tables differ in length");
|
||||
if (m_keys.size() > MAX_ENTRIES || m_enum_values.size() > MAX_ENTRIES)
|
||||
throw std::runtime_error("preset cache: dictionary is larger than the uint16 it is indexed with");
|
||||
if (m_enum_values.empty() || ! m_enum_values.front().empty())
|
||||
throw std::runtime_error("preset cache: dictionary is missing its unnamed-enum slot");
|
||||
// Resolved once per file: every option read after this is a vector index.
|
||||
m_defs.resize(m_keys.size());
|
||||
for (size_t i = 0; i < m_keys.size(); ++ i) {
|
||||
const ConfigOptionDef* def = print_config_def.get(m_keys[i]);
|
||||
m_defs[i] = (def != nullptr && uint16_t(def->type) == m_types[i]) ? def : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- one config -----------------------------------------------------------
|
||||
|
||||
static void save_enum_option(cereal::BinaryOutputArchive& ar, const ConfigOptionDef& def,
|
||||
const ConfigOption* opt, const CacheDictionary& dict)
|
||||
{
|
||||
const std::vector<int> values = enum_ints(def, opt);
|
||||
ar(uint32_t(values.size()));
|
||||
for (int value : values) {
|
||||
const uint16_t idx = dict.enum_index(enum_name_of(def, value));
|
||||
ar(idx);
|
||||
if (idx == CacheDictionary::ENUM_UNNAMED)
|
||||
ar(int32_t(value));
|
||||
}
|
||||
}
|
||||
|
||||
// `config` may be null, in which case the option is read and dropped.
|
||||
static void load_enum_option(cereal::BinaryInputArchive& ar, ConfigOptionType type,
|
||||
const ConfigOptionDef* def, DynamicPrintConfig* config,
|
||||
const CacheDictionary& dict)
|
||||
{
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
if (type == coEnum && cnt != 1)
|
||||
throw std::runtime_error("preset cache: a scalar enum carrying more than one value");
|
||||
// Every element is read whatever happens, so the stream stays in sync and
|
||||
// whatever follows this option still loads.
|
||||
bool usable = def != nullptr && config != nullptr;
|
||||
std::vector<int> values;
|
||||
values.reserve(cnt);
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
uint16_t idx = 0;
|
||||
ar(idx);
|
||||
if (! dict.valid_enum_index(idx))
|
||||
throw std::runtime_error("preset cache: enum value index past the end of the dictionary");
|
||||
if (idx == CacheDictionary::ENUM_UNNAMED) {
|
||||
// An int the writer could not name — a nil, or an option whose
|
||||
// definition carried no enum_keys_map. It travels verbatim.
|
||||
int32_t raw = 0;
|
||||
ar(raw);
|
||||
values.push_back(int(raw));
|
||||
continue;
|
||||
}
|
||||
if (! usable)
|
||||
continue; // the index above was this element's whole payload
|
||||
if (def->enum_keys_map == nullptr) {
|
||||
usable = false; // this build no longer maps this option's names
|
||||
continue;
|
||||
}
|
||||
const auto it = def->enum_keys_map->find(dict.enum_name_at(idx));
|
||||
if (it == def->enum_keys_map->end()) {
|
||||
usable = false; // a value this build dropped: the option goes with it
|
||||
continue;
|
||||
}
|
||||
values.push_back(it->second);
|
||||
}
|
||||
if (! usable)
|
||||
return;
|
||||
if (type == coEnum) {
|
||||
config->set_key_value(def->opt_key, new ConfigOptionEnumGeneric(def->enum_keys_map, values.front()));
|
||||
} else {
|
||||
auto* opt = def->nullable ? static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGenericNullable(def->enum_keys_map))
|
||||
: static_cast<ConfigOptionInts*>(new ConfigOptionEnumsGeneric(def->enum_keys_map));
|
||||
opt->values = std::move(values);
|
||||
config->set_key_value(def->opt_key, opt);
|
||||
}
|
||||
}
|
||||
|
||||
void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict)
|
||||
{
|
||||
struct Written { uint16_t idx; const ConfigOptionDef* def; const ConfigOption* opt; };
|
||||
std::vector<Written> written;
|
||||
written.reserve(config.size());
|
||||
for (auto it = config.cbegin(); it != config.cend(); ++ it)
|
||||
if (const ConfigOptionDef* def = print_config_def.get(it->first))
|
||||
written.push_back({ dict.key_index(it->first), def, it->second.get() });
|
||||
|
||||
ar(uint32_t(written.size()));
|
||||
for (const Written& w : written) {
|
||||
ar(w.idx);
|
||||
if (w.def->type == coEnum || w.def->type == coEnums)
|
||||
save_enum_option(ar, *w.def, w.opt, dict);
|
||||
else
|
||||
w.def->save_option_to_archive(ar, w.opt);
|
||||
}
|
||||
}
|
||||
|
||||
// `config` null means: read everything, keep nothing.
|
||||
static void read_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig* config, const CacheDictionary& dict)
|
||||
{
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
if (config != nullptr)
|
||||
config->clear();
|
||||
// Reused across the loop: constructing a ConfigOptionDef per dropped option
|
||||
// would allocate its strings and vectors for nothing.
|
||||
ConfigOptionDef scratch;
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
uint16_t idx = 0;
|
||||
ar(idx);
|
||||
if (! dict.valid_key_index(idx))
|
||||
throw std::runtime_error("preset cache: option index past the end of the dictionary");
|
||||
const ConfigOptionType type = dict.type_at(idx);
|
||||
const ConfigOptionDef* def = dict.def_at(idx);
|
||||
if (type == coEnum || type == coEnums) {
|
||||
load_enum_option(ar, type, def, config, dict);
|
||||
} else if (def != nullptr && config != nullptr) {
|
||||
config->set_key_value(def->opt_key, def->load_option_from_archive(ar));
|
||||
} else {
|
||||
// Read by the type the writer recorded, then drop: the same outcome
|
||||
// a JSON profile gets for an option this build no longer has.
|
||||
scratch.type = type;
|
||||
std::unique_ptr<ConfigOption> discard(scratch.load_option_from_archive(ar));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict)
|
||||
{
|
||||
read_config(ar, &config, dict);
|
||||
}
|
||||
|
||||
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict)
|
||||
{
|
||||
read_config(ar, nullptr, dict);
|
||||
}
|
||||
|
||||
// ---- The per-vendor cache file (<vendor>.opc) -----------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct CacheFileHeader {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint64_t data_size;
|
||||
uint32_t crc32;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(CacheFileHeader) == 20, "CacheFileHeader must be 20 bytes");
|
||||
|
||||
constexpr uint32_t CACHE_MAGIC = 0x4F52435A; // "ORCZ"
|
||||
// Bump when the wire format changes in a way the payload cannot describe
|
||||
// itself out of: reordering, removing or retyping a field of a hand-written
|
||||
// serialize() (VendorProfile and its nested types, CachedPreset via
|
||||
// save_entries below), or a change to the cache's own layout or the
|
||||
// meaning of its stamps. Option-schema drift is NOT such a change — the
|
||||
// dictionary handles it, which is why this no longer moves every release.
|
||||
constexpr uint32_t CACHE_VERSION = 1;
|
||||
|
||||
// A stamp-string read that refuses an absurd length before allocating anything.
|
||||
// The stamps are read from files named from the outside (peek_version is
|
||||
// pointed at whatever <vendor>.opc a directory holds), so the length word may
|
||||
// be arbitrary bytes — and a resize to a garbage 64-bit length does not fail as
|
||||
// a catchable bad_alloc here, it takes the app down through the out-of-memory
|
||||
// handler. A vendor name or profile version is a short token; anything longer
|
||||
// is not a cache this build wrote.
|
||||
std::string read_bounded_string(cereal::BinaryInputArchive& ar)
|
||||
{
|
||||
constexpr uint64_t MAX_STAMP_LEN = 1024;
|
||||
cereal::size_type len = 0;
|
||||
ar(cereal::make_size_tag(len));
|
||||
if (uint64_t(len) > MAX_STAMP_LEN)
|
||||
throw std::runtime_error("preset cache: string length out of bounds");
|
||||
std::string s(size_t(len), '\0');
|
||||
ar(cereal::binary_data(s.data(), size_t(len)));
|
||||
return s;
|
||||
}
|
||||
|
||||
// The prologue every cache reader starts with: the format version, then the
|
||||
// vendor's identity. Returns the vendor version stamped on a body this build can
|
||||
// read, empty on anything else — which is the same answer as "not this vendor".
|
||||
std::string read_cache_stamps(cereal::BinaryInputArchive& ar, const std::string& expected_vendor_name)
|
||||
{
|
||||
// The version is judged before anything variable-length is read: on a body
|
||||
// that is not a per-vendor cache of this version, the bytes where a string
|
||||
// length would sit may be arbitrary framing.
|
||||
uint32_t cache_version = 0;
|
||||
ar(cache_version);
|
||||
if (cache_version != CACHE_VERSION)
|
||||
return {};
|
||||
const std::string vendor_name = read_bounded_string(ar);
|
||||
const std::string vendor_version = read_bounded_string(ar);
|
||||
if (vendor_name != expected_vendor_name)
|
||||
return {};
|
||||
return vendor_version;
|
||||
}
|
||||
|
||||
// A cache stays usable as long as it was built from a vendor profile at least
|
||||
// as new as the one now on disk. Profiles whose version is invalid cannot be
|
||||
// judged this way and are never served from cache; where no profile sits
|
||||
// beside the cache at all, nothing can be newer than it — that state is passed
|
||||
// as Semver::inf(), which no real profile can carry (an invalid version could
|
||||
// not say it apart from "profile there but unjudgeable", and zero would
|
||||
// collide with a genuine "0.0.0"). This is the serve rule; the install rule
|
||||
// (cache_covers in PresetBundle.cpp) deliberately reads an unjudgeable profile
|
||||
// the other way, so the two are not one function.
|
||||
bool cache_covers_version(const std::string& cached, const Semver& on_disk)
|
||||
{
|
||||
if (on_disk == Semver::inf())
|
||||
return true; // before parsing `cached`: nothing exists that the stamp must cover
|
||||
if (! on_disk.valid())
|
||||
return false;
|
||||
const auto cached_ver = Semver::parse(cached);
|
||||
return cached_ver && *cached_ver >= on_disk;
|
||||
}
|
||||
|
||||
// CachedPreset on the wire: all fields, declaration order, in one place.
|
||||
// `config` writes, reads or skips the config sitting in the middle of that
|
||||
// order — the three things a reader can want to do with it — so save, load and
|
||||
// the name peek below cannot drift apart. Keep in sync with the struct in
|
||||
// PresetCacheFormat.hpp and bump CACHE_VERSION on change. Written here rather
|
||||
// than as a serialize() member because the config needs the file's dictionary,
|
||||
// which cereal cannot thread through one.
|
||||
template<class Archive, class Entry, class ConfigFn>
|
||||
void visit_entry(Archive& ar, Entry& e, ConfigFn&& config)
|
||||
{
|
||||
ar(e.name, e.sub_path);
|
||||
config();
|
||||
ar(e.inherits, e.description, e.instantiation, e.setting_id, e.filament_id, e.renamed_from);
|
||||
}
|
||||
|
||||
// The count comes from a file that has already passed magic and CRC, but a
|
||||
// reserve is a promise to allocate: cap it and let push_back grow the rest.
|
||||
constexpr uint32_t MAX_RESERVED_ENTRIES = 4096;
|
||||
|
||||
void save_entries(cereal::BinaryOutputArchive& ar,
|
||||
const std::vector<CachedPreset>& entries,
|
||||
const CacheDictionary& dict)
|
||||
{
|
||||
ar(uint32_t(entries.size()));
|
||||
for (const CachedPreset& e : entries)
|
||||
visit_entry(ar, e, [&] { save_config(ar, e.config_src, dict); });
|
||||
}
|
||||
|
||||
void load_entries(cereal::BinaryInputArchive& ar,
|
||||
std::vector<CachedPreset>& entries,
|
||||
const CacheDictionary& dict)
|
||||
{
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
entries.clear();
|
||||
entries.reserve(std::min(cnt, MAX_RESERVED_ENTRIES));
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
CachedPreset e;
|
||||
visit_entry(ar, e, [&] { load_config(ar, e.config_src, dict); });
|
||||
entries.push_back(std::move(e));
|
||||
}
|
||||
}
|
||||
|
||||
// Read a raw cache body: verify magic, size, CRC.
|
||||
bool read_cache_blob(const std::string& path, std::string& out_blob)
|
||||
{
|
||||
try {
|
||||
boost::nowide::ifstream ifs(path, std::ios::binary);
|
||||
if (!ifs.is_open())
|
||||
return false;
|
||||
CacheFileHeader fhdr;
|
||||
if (!ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)))
|
||||
return false;
|
||||
if (fhdr.magic != CACHE_MAGIC)
|
||||
return false;
|
||||
// data_size is 8 bytes from a file nothing has authenticated yet, and
|
||||
// it is about to size an allocation. The body is the whole of the file
|
||||
// behind the header — anything else is not a cache this build wrote.
|
||||
ifs.seekg(0, std::ios::end);
|
||||
const std::streamoff file_size = ifs.tellg();
|
||||
if (file_size < std::streamoff(sizeof(fhdr)) ||
|
||||
fhdr.data_size == 0 ||
|
||||
fhdr.data_size != uint64_t(file_size) - sizeof(fhdr))
|
||||
return false;
|
||||
ifs.seekg(sizeof(fhdr), std::ios::beg);
|
||||
out_blob.assign(fhdr.data_size, '\0');
|
||||
if (!ifs.read(&out_blob[0], static_cast<std::streamsize>(fhdr.data_size)))
|
||||
return false;
|
||||
boost::crc_32_type crc;
|
||||
crc.process_bytes(out_blob.data(), out_blob.size());
|
||||
if (crc.checksum() != fhdr.crc32) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: CRC mismatch: " << path;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: read failed (" << path << "): " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Write a cache body behind the standard 20-byte file header. False when the
|
||||
// file could not be opened or written whole.
|
||||
bool write_cache_blob(const std::string& path, const std::string& blob)
|
||||
{
|
||||
boost::crc_32_type crc;
|
||||
crc.process_bytes(blob.data(), blob.size());
|
||||
// Written beside the target and moved into place, as AppConfig::save does:
|
||||
// a cache is truncated and rewritten in full, so a write that dies partway
|
||||
// would otherwise leave a header claiming more body than the file holds.
|
||||
// The PID suffix also keeps two instances writing the same vendor from
|
||||
// interleaving.
|
||||
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
|
||||
try {
|
||||
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
||||
{
|
||||
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
|
||||
if (!ofs.is_open()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path;
|
||||
return false;
|
||||
}
|
||||
CacheFileHeader fhdr;
|
||||
fhdr.magic = CACHE_MAGIC;
|
||||
fhdr.version = CACHE_VERSION;
|
||||
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
||||
fhdr.crc32 = crc.checksum();
|
||||
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
||||
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
|
||||
ofs.close(); // flush; close() raises failbit on error
|
||||
if (! ofs.good()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")";
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(tmp_path, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (const std::error_code ec = rename_file(tmp_path, path)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message();
|
||||
boost::system::error_code rm;
|
||||
boost::filesystem::remove(tmp_path, rm);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove(tmp_path, ec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// static
|
||||
bool VendorCacheFile::save(const std::string& path, const std::string& vendor_name,
|
||||
const std::string& vendor_version, const VendorCacheData& data)
|
||||
{
|
||||
try {
|
||||
// Collected before anything is written: the dictionary sits ahead of the
|
||||
// entries so a reader resolves it once and then indexes.
|
||||
CacheDictionary dict;
|
||||
for (const std::vector<CachedPreset>* entries : { &data.process_entries, &data.filament_entries, &data.machine_entries })
|
||||
for (const CachedPreset& e : *entries)
|
||||
dict.collect(e.config_src);
|
||||
|
||||
std::ostringstream body(std::ios::binary);
|
||||
{
|
||||
cereal::BinaryOutputArchive ar(body);
|
||||
ar(CACHE_VERSION);
|
||||
ar(vendor_name, vendor_version);
|
||||
dict.save(ar);
|
||||
ar(data.vendors);
|
||||
save_entries(ar, data.process_entries, dict);
|
||||
save_entries(ar, data.filament_entries, dict);
|
||||
save_entries(ar, data.machine_entries, dict);
|
||||
ar(data.parse_errors);
|
||||
}
|
||||
return write_cache_blob(path, body.str());
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: failed to save vendor cache " << path << ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool VendorCacheFile::load(const std::string& path, const std::string& expected_vendor_name,
|
||||
const Semver& expected_vendor_version, VendorCacheData& data)
|
||||
{
|
||||
std::string blob;
|
||||
if (! read_cache_blob(path, blob))
|
||||
return false;
|
||||
try {
|
||||
// Read in place: an istringstream would copy the blob once more just to
|
||||
// stream over it.
|
||||
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
const std::string vendor_version = read_cache_stamps(ar, expected_vendor_name);
|
||||
if (vendor_version.empty() || ! cache_covers_version(vendor_version, expected_vendor_version))
|
||||
return false;
|
||||
CacheDictionary dict;
|
||||
dict.load(ar);
|
||||
ar(data.vendors);
|
||||
load_entries(ar, data.process_entries, dict);
|
||||
load_entries(ar, data.filament_entries, dict);
|
||||
load_entries(ar, data.machine_entries, dict);
|
||||
ar(data.parse_errors);
|
||||
if (data.vendors.find(expected_vendor_name) == data.vendors.end())
|
||||
throw std::runtime_error("vendor cache does not carry its own vendor profile");
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: rejecting vendor cache " << path << ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
std::string VendorCacheFile::peek_version(const std::string& path, const std::string& expected_vendor_name)
|
||||
{
|
||||
try {
|
||||
boost::nowide::ifstream ifs(path, std::ios::binary);
|
||||
CacheFileHeader fhdr;
|
||||
if (! ifs.read(reinterpret_cast<char*>(&fhdr), sizeof(fhdr)) || fhdr.magic != CACHE_MAGIC)
|
||||
return {};
|
||||
// Only the head of the body is read, and its CRC left unverified: the
|
||||
// stamps sit at the front, and this answers "what version is this?"
|
||||
// without paying for tens of megabytes. Callers that need to know the
|
||||
// file is whole use usable_version instead.
|
||||
std::string head(static_cast<size_t>(std::min<uint64_t>(fhdr.data_size, 1024)), '\0');
|
||||
if (! ifs.read(&head[0], static_cast<std::streamsize>(head.size())))
|
||||
return {};
|
||||
std::istringstream body(head, std::ios::binary);
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
return read_cache_stamps(ar, expected_vendor_name);
|
||||
} catch (const std::exception&) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
Semver VendorCacheFile::usable_version(const std::string& path, const std::string& expected_vendor_name)
|
||||
{
|
||||
std::string blob;
|
||||
if (! read_cache_blob(path, blob))
|
||||
return Semver::invalid();
|
||||
try {
|
||||
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
const auto ver = Semver::parse(read_cache_stamps(ar, expected_vendor_name));
|
||||
return ver ? *ver : Semver::invalid();
|
||||
} catch (const std::exception&) {
|
||||
return Semver::invalid();
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool VendorCacheFile::carries_preset(const std::string& path, const std::string& vendor_name,
|
||||
Preset::Type type, const std::string& preset_name)
|
||||
{
|
||||
std::string blob;
|
||||
if (! read_cache_blob(path, blob))
|
||||
return false;
|
||||
try {
|
||||
boost::iostreams::stream<boost::iostreams::array_source> body(blob.data(), blob.size());
|
||||
cereal::BinaryInputArchive ar(body);
|
||||
if (read_cache_stamps(ar, vendor_name).empty())
|
||||
return false;
|
||||
CacheDictionary dict;
|
||||
dict.load(ar);
|
||||
VendorMap vendors;
|
||||
ar(vendors);
|
||||
// Reused: every entry overwrites it, and only its name is ever looked at.
|
||||
CachedPreset entry;
|
||||
// Written in this order by save. The list that could carry the preset
|
||||
// is the last one worth reading.
|
||||
for (Preset::Type kind : { Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_PRINTER }) {
|
||||
uint32_t cnt = 0;
|
||||
ar(cnt);
|
||||
for (uint32_t i = 0; i < cnt; ++ i) {
|
||||
visit_entry(ar, entry, [&] { skip_config(ar, dict); });
|
||||
if (kind == type && entry.name == preset_name)
|
||||
return true;
|
||||
}
|
||||
if (kind == type)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not read preset names from " << path << ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,192 @@
|
||||
#ifndef slic3r_PresetCacheFormat_hpp_
|
||||
#define slic3r_PresetCacheFormat_hpp_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <cereal/archives/binary.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Preset.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// How the preset cache writes a DynamicPrintConfig.
|
||||
//
|
||||
// Not through the global cereal hooks in PrintConfig.hpp: those key an option by
|
||||
// its serialization_key_ordinal, which ConfigDef::add assigns by declaration
|
||||
// order at static-init time. Inserting one option into the middle of
|
||||
// PrintConfig.cpp shifts every later ordinal, and the lookup on the way back in
|
||||
// then SUCCEEDS on the wrong option — where the two share a type, and hundreds
|
||||
// of coFloat/coBool/coInt options do, the bytes deserialize cleanly into the
|
||||
// wrong key. Silently wrong print settings, no error. Those hooks are also the
|
||||
// undo/redo wire format, where the process cannot change underneath them, so
|
||||
// they stay as they are and the cache keys by name instead.
|
||||
//
|
||||
// Names are not repeated per preset. Each cache file carries one dictionary of
|
||||
// the distinct opt_keys it uses, the type each was written as, and the distinct
|
||||
// enum value names; an option on the wire is then a uint16 index into it plus
|
||||
// its value. The dictionary is resolved to this build's option definitions once
|
||||
// per file, after which reading an option is a vector index.
|
||||
class CacheDictionary
|
||||
{
|
||||
public:
|
||||
CacheDictionary();
|
||||
|
||||
// Index reserved in the enum table for an int the writing build could not
|
||||
// name — a nullable option's nil, or a definition carrying no
|
||||
// enum_keys_map. The raw int32 follows it on the wire and is loaded
|
||||
// verbatim, so those values survive too.
|
||||
static constexpr uint16_t ENUM_UNNAMED = 0;
|
||||
|
||||
// ---- writing ----
|
||||
|
||||
// Record every key and enum value `config` uses. Call for every config that
|
||||
// will be written, before writing the dictionary.
|
||||
void collect(const DynamicPrintConfig& config);
|
||||
|
||||
uint16_t key_index(const t_config_option_key& key) const;
|
||||
// ENUM_UNNAMED for an empty name or one that was never collected.
|
||||
uint16_t enum_index(const std::string& name) const;
|
||||
|
||||
// ---- reading ----
|
||||
|
||||
// The definition an index resolves to in THIS build, or nullptr where the
|
||||
// key is unknown here or is now defined with a different type. A nullptr
|
||||
// entry's value is still read — using type_at(idx), the type the writer
|
||||
// recorded — and then dropped, which is what a JSON profile gets for an
|
||||
// option this build no longer has.
|
||||
const ConfigOptionDef* def_at(uint16_t idx) const { return m_defs[idx]; }
|
||||
ConfigOptionType type_at(uint16_t idx) const { return ConfigOptionType(m_types[idx]); }
|
||||
const std::string& enum_name_at(uint16_t idx) const { return m_enum_values[idx]; }
|
||||
// m_defs, not m_keys: only load() sizes it, so this is false for every index
|
||||
// on a dictionary that was collected rather than read.
|
||||
bool valid_key_index(uint16_t idx) const { return size_t(idx) < m_defs.size(); }
|
||||
bool valid_enum_index(uint16_t idx) const { return size_t(idx) < m_enum_values.size(); }
|
||||
|
||||
// The layout these two agree on is covered by CACHE_VERSION (PresetCacheFormat.cpp);
|
||||
// bump it when they change.
|
||||
// Throws when either table outgrew the uint16 the wire format indexes it
|
||||
// with. Both are bounded by the option count (912 at the time of writing), so
|
||||
// that is a build-time failure in CI, not a runtime one.
|
||||
void save(cereal::BinaryOutputArchive& ar) const;
|
||||
// Throws on a dictionary that cannot be indexed as written.
|
||||
void load(cereal::BinaryInputArchive& ar);
|
||||
|
||||
private:
|
||||
// Indices are uint16, so a table may hold at most this many entries.
|
||||
static constexpr size_t MAX_ENTRIES = 0xFFFF;
|
||||
|
||||
std::vector<std::string> m_keys;
|
||||
// ConfigOptionType, as written. Sixteen bits, not eight: coVectorType is
|
||||
// 0x4000, so every vector type — coFloats, coEnums, coStrings — is above
|
||||
// 255, and a byte would fold each one onto its scalar counterpart.
|
||||
std::vector<uint16_t> m_types;
|
||||
std::vector<std::string> m_enum_values; // [ENUM_UNNAMED] is always empty
|
||||
|
||||
// Writing.
|
||||
std::unordered_map<std::string, uint16_t> m_key_index;
|
||||
std::unordered_map<std::string, uint16_t> m_enum_index;
|
||||
// Reading, resolved once by load().
|
||||
std::vector<const ConfigOptionDef*> m_defs;
|
||||
};
|
||||
|
||||
// One config, keyed through `dict`. Options print_config_def does not know are
|
||||
// not written: nothing could give them a type on the way back in.
|
||||
void save_config(cereal::BinaryOutputArchive& ar, const DynamicPrintConfig& config, const CacheDictionary& dict);
|
||||
// Throws only on a payload that cannot be indexed; an option this build cannot
|
||||
// place is dropped, not fatal.
|
||||
void load_config(cereal::BinaryInputArchive& ar, DynamicPrintConfig& config, const CacheDictionary& dict);
|
||||
// Consume one config without building it, for a reader that only wants what
|
||||
// comes after.
|
||||
void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
|
||||
|
||||
// One preset as its JSON subfile states it: the config diff, the name of the
|
||||
// preset it inherits, and the parse metadata — everything the parse phase of
|
||||
// load_vendor_configs_from_json extracts and nothing it derives. Inheritance
|
||||
// is resolved when the entry is installed, against whatever filament library
|
||||
// is loaded then, so a cache carries no other vendor's values and no other
|
||||
// vendor's update can make it stale.
|
||||
// Written and read by visit_entry in PresetCacheFormat.cpp, which lists every
|
||||
// field below in this order — once, for the save, the load and the name peek alike.
|
||||
struct CachedPreset
|
||||
{
|
||||
std::string name;
|
||||
std::string sub_path; // path under the vendor's directory
|
||||
DynamicPrintConfig config_src; // the preset's own diff, nothing inherited
|
||||
std::string inherits;
|
||||
std::string description;
|
||||
std::string instantiation; // "true"/"false" as stated; anything else was already counted as a parse error
|
||||
std::string setting_id;
|
||||
std::string filament_id;
|
||||
std::vector<std::string> renamed_from;
|
||||
};
|
||||
|
||||
// What one per-vendor cache file carries besides its stamps: the vendor profile
|
||||
// map, the presets in source form, and how many errors their parse counted.
|
||||
struct VendorCacheData
|
||||
{
|
||||
VendorMap vendors;
|
||||
std::vector<CachedPreset> process_entries;
|
||||
std::vector<CachedPreset> filament_entries;
|
||||
std::vector<CachedPreset> machine_entries;
|
||||
uint64_t parse_errors = 0;
|
||||
};
|
||||
|
||||
// A per-vendor preset cache file (<vendor>.opc): a 20-byte header (magic, format
|
||||
// version, body size, CRC) framing one cereal body — stamps (format version,
|
||||
// vendor name, vendor profile version), the option dictionary, then the
|
||||
// VendorCacheData. Everything about those bytes lives here; when a vendor is
|
||||
// served from its cache, and how entries install into a bundle, is
|
||||
// PresetBundle's business.
|
||||
class VendorCacheFile
|
||||
{
|
||||
public:
|
||||
// Save one vendor (vendor_name at vendor_version). False when the file
|
||||
// could not be written whole.
|
||||
static bool save(const std::string& path, const std::string& vendor_name,
|
||||
const std::string& vendor_version, const VendorCacheData& data);
|
||||
|
||||
// Read a whole cache into `data`. False — with `data` in an unspecified
|
||||
// state — unless the file is a cache this build wrote, its CRC holds, it
|
||||
// names this vendor, it was built from a vendor profile at least as new as
|
||||
// `expected_vendor_version`, and it carries its own vendor profile. An
|
||||
// invalid expected version (a profile whose version
|
||||
// cannot be judged) is never served from cache; Semver::inf() (no profile
|
||||
// beside the cache at all) accepts whatever is cached.
|
||||
static bool load(const std::string& path, const std::string& expected_vendor_name,
|
||||
const Semver& expected_vendor_version, VendorCacheData& data);
|
||||
|
||||
// Read the profile version a cache was stamped with, without deserializing
|
||||
// its presets. Empty if the file is unreadable, not a cache this build
|
||||
// understands, or not this vendor's. This is how an installed vendor's
|
||||
// version is known when only its cache is installed.
|
||||
static std::string peek_version(const std::string& path, const std::string& expected_vendor_name);
|
||||
|
||||
// The profile version an installed cache can actually be served at, or an
|
||||
// invalid Semver when the file is not a cache this build can read. Unlike
|
||||
// peek_version this verifies the body's CRC, at the cost of reading the
|
||||
// whole file: where the cache is the vendor's whole installation, "a file
|
||||
// is there" is not enough to call it installed, and a vendor wrongly
|
||||
// believed installed is never repaired.
|
||||
static Semver usable_version(const std::string& path, const std::string& expected_vendor_name);
|
||||
|
||||
// Whether a cache carries a preset of `type` under `preset_name`, without
|
||||
// installing any of them. False when the file is not a cache this build can
|
||||
// read. The three kinds are written in one stream, so reaching the machines
|
||||
// means reading past the processes and filaments — their configs are consumed
|
||||
// and dropped rather than built. This is how a build that ships caches instead
|
||||
// of preset JSONs answers "which vendor carries this preset?".
|
||||
static bool carries_preset(const std::string& path, const std::string& vendor_name,
|
||||
Preset::Type type, const std::string& preset_name);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_PresetCacheFormat_hpp_
|
||||
+40
-22
@@ -292,6 +292,14 @@ 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"
|
||||
@@ -412,6 +420,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"
|
||||
@@ -1093,13 +1102,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);
|
||||
}
|
||||
@@ -1118,6 +1128,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 {};
|
||||
}
|
||||
|
||||
@@ -4175,6 +4201,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) {
|
||||
@@ -4186,30 +4218,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);
|
||||
}
|
||||
@@ -6067,7 +6085,7 @@ BoundingBoxf3 PrintInstance::get_bounding_box() const {
|
||||
|
||||
Polygon PrintInstance::get_convex_hull_2d() {
|
||||
Polygon poly = print_object->model_object()->convex_hull_2d(model_instance->get_matrix());
|
||||
poly.douglas_peucker(0.1);
|
||||
poly.douglas_peucker(scale_(0.1));
|
||||
return poly;
|
||||
}
|
||||
|
||||
|
||||
@@ -1001,8 +1001,8 @@ public:
|
||||
// If preview_data is not null, the preview_data is filled in for the G-code visualization (not used by the command line Slic3r).
|
||||
std::string export_gcode(const std::string& path_template, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
|
||||
//return 0 means successful
|
||||
int export_cached_data(const std::string& dir_path, bool with_space=false);
|
||||
int load_cached_data(const std::string& directory);
|
||||
int export_cached_data(const std::string& dir_path, bool with_space=false) override;
|
||||
int load_cached_data(const std::string& directory) override;
|
||||
|
||||
// methods for handling state
|
||||
bool is_step_done(PrintStep step) const { return Inherited::is_step_done(step); }
|
||||
|
||||
@@ -74,6 +74,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
|
||||
@@ -2029,6 +2031,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;
|
||||
@@ -3549,6 +3558,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");
|
||||
@@ -4332,6 +4352,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.");
|
||||
@@ -4365,7 +4394,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");
|
||||
@@ -5798,12 +5827,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. });
|
||||
@@ -6057,7 +6084,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
|
||||
@@ -6634,6 +6661,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)");
|
||||
@@ -8528,10 +8566,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",
|
||||
@@ -9619,6 +9659,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",
|
||||
@@ -10776,6 +10818,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
|
||||
@@ -11853,7 +11905,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,
|
||||
@@ -906,6 +929,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);
|
||||
@@ -1148,6 +1174,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionFloat, extra_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))
|
||||
@@ -1333,6 +1360,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))
|
||||
@@ -1614,7 +1642,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))
|
||||
@@ -1727,6 +1755,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))
|
||||
|
||||
@@ -2499,6 +2528,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
|
||||
@@ -2518,7 +2596,8 @@ namespace cereal {
|
||||
archive(serialization_key_ordinal);
|
||||
assert(serialization_key_ordinal > 0);
|
||||
auto it = Slic3r::print_config_def.by_serialization_key_ordinal.find(serialization_key_ordinal);
|
||||
assert(it != Slic3r::print_config_def.by_serialization_key_ordinal.end());
|
||||
if (it == Slic3r::print_config_def.by_serialization_key_ordinal.end())
|
||||
throw std::runtime_error("VendorCache: unknown serialization_key_ordinal " + std::to_string(serialization_key_ordinal) + " - cache is stale");
|
||||
config.set_key_value(it->second->opt_key, it->second->load_option_from_archive(archive));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1290,6 +1290,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"
|
||||
@@ -1524,6 +1525,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"
|
||||
@@ -3933,7 +3935,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;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <libslic3r/SLA/SupportTreeBuilder.hpp>
|
||||
#include <libslic3r/SLA/SupportTreeBuildsteps.hpp>
|
||||
|
||||
@@ -190,6 +190,19 @@ public:
|
||||
os << self.to_string();
|
||||
return os;
|
||||
}
|
||||
|
||||
// cereal: round-trip through the standard 3-part string (major.minor.patch).
|
||||
// to_string() uses a BBS 4-part format that semver_parse() cannot read back.
|
||||
template<class Archive>
|
||||
std::string save_minimal(const Archive&) const { return to_string_sf(); }
|
||||
template<class Archive>
|
||||
void load_minimal(const Archive&, const std::string& s) {
|
||||
auto v = Semver::parse(s);
|
||||
if (! v)
|
||||
throw std::runtime_error("Semver: cannot parse serialized version: " + s);
|
||||
*this = std::move(*v);
|
||||
}
|
||||
|
||||
private:
|
||||
semver_t ver;
|
||||
|
||||
|
||||
@@ -51,9 +51,4 @@
|
||||
// Enable extension of tool position imgui dialog to show actual speed profile
|
||||
#define ENABLE_ACTUAL_SPEED_DEBUG 1
|
||||
|
||||
// Disable layout inspector for public release
|
||||
#if BBL_RELEASE_TO_PUBLIC
|
||||
#define WXINSPECTOR_DISABLE
|
||||
#endif
|
||||
|
||||
#endif // _prusaslicer_technologies_h_
|
||||
|
||||
@@ -1499,6 +1499,13 @@ static std::vector<Polygons> make_loops(
|
||||
Polygons &polygons = layers[line_idx];
|
||||
polygons = make_loops(lines[line_idx]);
|
||||
|
||||
// Orca: A planar quad represented by two triangles contributes a point where the
|
||||
// slicing plane crosses the shared diagonal. After rounding to coord_t this
|
||||
// point may be very slightly off the otherwise straight contour edge. Apart
|
||||
// from being redundant, such points make the subsequent contour
|
||||
// simplification depend on the slice height (and may move seam candidates).
|
||||
remove_collinear(polygons);
|
||||
|
||||
auto this_mode = line_idx < params.slicing_mode_normal_below_layer ? params.mode_below : params.mode;
|
||||
if (! polygons.empty()) {
|
||||
if (this_mode == MeshSlicingParams::SlicingMode::Positive) {
|
||||
|
||||
+38
-5
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <iomanip>
|
||||
#include <locale>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
#include <functional>
|
||||
#include <type_traits>
|
||||
@@ -18,6 +19,7 @@
|
||||
#include <openssl/md5.h>
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Semver.hpp"
|
||||
|
||||
//define CLI errors
|
||||
|
||||
@@ -722,11 +724,42 @@ void copy_directory_recursively(const boost::filesystem::path& source,
|
||||
std::function<bool(const std::string)> filter = nullptr,
|
||||
bool merge_mode = false);
|
||||
|
||||
// Install vendor bundles from resources directory to data directory
|
||||
// bundle_names: vector of vendor bundle names (without .json extension)
|
||||
// resource_subdir: subdirectory under resources_dir() (default: "profiles")
|
||||
// data_subdir: subdirectory under data_dir() (default: "system")
|
||||
// Returns: true if all bundles installed successfully, false otherwise
|
||||
// ---- Vendor installation on disk ------------------------------------------
|
||||
// How a vendor bundle is installed from resources into data_dir()/system: as
|
||||
// its profile and preset JSONs or, in a build that ships preset caches, as its
|
||||
// .opc preset cache alone. Loading what is installed is PresetBundle's business;
|
||||
// the cache file format itself is VendorCacheFile's (PresetCacheFormat.hpp).
|
||||
|
||||
// True if `vendor` is installed in data_dir()/system. A build that ships preset
|
||||
// caches installs the cache alone, so it — not the profile — marks a vendor
|
||||
// installed; a cache this build cannot read marks nothing.
|
||||
bool is_vendor_installed(const std::string& vendor);
|
||||
|
||||
// The version the installed vendor would be loaded at: its cache's stamp while
|
||||
// that covers the profile beside it, the profile's own version once it does not.
|
||||
// Invalid Semver if neither form is installed.
|
||||
Semver installed_vendor_version(const std::string& vendor);
|
||||
|
||||
// Remove every form `vendor` can be installed as from data_dir()/system: its
|
||||
// profile, its preset cache, and its preset directory.
|
||||
void remove_installed_vendor(const std::string& vendor);
|
||||
|
||||
// The vendors `dir` holds, sorted: one is named by its profile or, in a build that
|
||||
// ships preset caches instead of the raw profile JSONs, by its cache alone.
|
||||
std::set<std::string> vendor_names_in(const boost::filesystem::path& dir);
|
||||
|
||||
// The version a build ships `vendor` at: whichever of its preset cache and its
|
||||
// profile is newer, that being the one installing lays down. Invalid Semver if the
|
||||
// build ships neither.
|
||||
Semver resource_vendor_version(const std::string& vendor);
|
||||
|
||||
// Install vendors from the resources directory into the data directory, each as
|
||||
// its preset cache or as its profile and preset JSONs — whichever of the two the
|
||||
// build ships at the newer version. Anything the previous install of that vendor
|
||||
// left behind goes, so only the form just installed is there to be loaded.
|
||||
// bundle_names: vendor names, without extension.
|
||||
// Every bundle that can be installed is, whatever the others do. Returns false
|
||||
// if any named bundle could not be installed.
|
||||
bool install_vendor_bundles_from_resources(const std::vector<std::string>& bundle_names,
|
||||
const std::string& resource_subdir = "profiles",
|
||||
const std::string& data_subdir = "system");
|
||||
|
||||
+141
-13
@@ -17,6 +17,10 @@
|
||||
#include "Platform.hpp"
|
||||
#include "Time.hpp"
|
||||
#include "libslic3r.h"
|
||||
// For the vendor-installation helpers: the vendor profile version
|
||||
// (get_version_from_json) and the preset cache stamp (VendorCacheFile).
|
||||
#include "Preset.hpp"
|
||||
#include "PresetCacheFormat.hpp"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "MacUtils.hpp"
|
||||
@@ -1724,6 +1728,85 @@ void copy_directory_recursively(const boost::filesystem::path& source,
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Vendor installation on disk ------------------------------------------
|
||||
|
||||
// Whether a cache stamped `cache_ver` still speaks for a vendor whose profile on
|
||||
// disk claims `profile_ver`: it does unless the profile has moved ahead of it. A
|
||||
// profile that is missing or carries no judgeable version cannot be ahead of
|
||||
// anything. The one rule behind both "which form gets installed" and "which form
|
||||
// is installed"; they must not drift apart. Deliberately NOT the serve rule
|
||||
// (VendorCacheFile::load), which refuses an unjudgeable profile instead.
|
||||
static bool cache_covers(const Semver& cache_ver, const Semver& profile_ver)
|
||||
{
|
||||
return cache_ver.valid() && (! profile_ver.valid() || cache_ver >= profile_ver);
|
||||
}
|
||||
|
||||
bool is_vendor_installed(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
|
||||
// A cache is the whole of a cache-only installation, so a file this build
|
||||
// cannot serve the vendor from is not an installation. Left counted as one,
|
||||
// the updater would never lay a working copy down.
|
||||
return boost::filesystem::exists(dir / (vendor + ".json"))
|
||||
|| VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor).valid();
|
||||
}
|
||||
|
||||
Semver installed_vendor_version(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
|
||||
const boost::filesystem::path json = dir / (vendor + ".json");
|
||||
// Guarded: get_version_from_json logs an error and throws-and-catches its way
|
||||
// to an invalid version on a file that is not there, and a cache-only vendor
|
||||
// never has one.
|
||||
const Semver from_json = boost::filesystem::exists(json) ? get_version_from_json(json.string()) : Semver();
|
||||
const Semver from_cache = VendorCacheFile::usable_version((dir / (vendor + ".opc")).string(), vendor);
|
||||
// Whichever form a load would serve.
|
||||
return cache_covers(from_cache, from_json) ? from_cache : from_json;
|
||||
}
|
||||
|
||||
void remove_installed_vendor(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(data_dir()) / PRESET_SYSTEM_DIR;
|
||||
boost::filesystem::remove(dir / (vendor + ".json"));
|
||||
boost::filesystem::remove(dir / (vendor + ".opc"));
|
||||
if (boost::filesystem::exists(dir / vendor))
|
||||
boost::filesystem::remove_all(dir / vendor);
|
||||
}
|
||||
|
||||
std::set<std::string> vendor_names_in(const boost::filesystem::path& dir)
|
||||
{
|
||||
std::set<std::string> names;
|
||||
for (auto& dir_entry : boost::filesystem::directory_iterator(dir)) {
|
||||
const auto& path = dir_entry.path();
|
||||
if (Slic3r::is_json_file(path.string()) || path.extension() == ".opc")
|
||||
names.insert(path.stem().string());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
// A vendor's preset cache is the whole of its installation: it carries the presets,
|
||||
// the vendor profile and the version they were built at, so where one ships nothing
|
||||
// else needs copying. Unless the profile beside it claims a newer version — a cache
|
||||
// generated before that profile was bumped is out of date, and a cache that cannot
|
||||
// be read is no installation at all — and the vendor is installed the way it was
|
||||
// before caches existed, as its profile and the preset JSONs it points at. Returns
|
||||
// the version the cache is stamped with, invalid when it is not the form to install.
|
||||
static Semver installable_cache_version(const boost::filesystem::path& dir, const std::string& vendor)
|
||||
{
|
||||
const auto cache_ver = Semver::parse(VendorCacheFile::peek_version((dir / (vendor + ".opc")).string(), vendor));
|
||||
if (! cache_ver)
|
||||
return Semver::invalid();
|
||||
const Semver profile_ver = get_version_from_json((dir / (vendor + ".json")).string());
|
||||
return cache_covers(*cache_ver, profile_ver) ? *cache_ver : Semver::invalid();
|
||||
}
|
||||
|
||||
Semver resource_vendor_version(const std::string& vendor)
|
||||
{
|
||||
const boost::filesystem::path dir = boost::filesystem::path(resources_dir()) / "profiles";
|
||||
const Semver ver = installable_cache_version(dir, vendor);
|
||||
return ver.valid() ? ver : get_version_from_json((dir / (vendor + ".json")).string());
|
||||
}
|
||||
|
||||
bool install_vendor_bundles_from_resources(
|
||||
const std::vector<std::string>& bundle_names,
|
||||
const std::string& resource_subdir,
|
||||
@@ -1736,37 +1819,82 @@ bool install_vendor_bundles_from_resources(
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << "Installing " << bundle_names.size() << " bundles from resources...";
|
||||
|
||||
// One vendor that cannot be installed is one vendor missing, not a reason to
|
||||
// leave the rest uninstalled. The caller is told, and every bundle that can
|
||||
// be laid down is.
|
||||
bool all_installed = true;
|
||||
|
||||
for (const auto &bundle : bundle_names) {
|
||||
try {
|
||||
if (bundle.empty()) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Refusing to install a bundle with no name";
|
||||
all_installed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Install the JSON file
|
||||
auto path_in_rsrc = (rsrc_path / bundle).replace_extension(".json");
|
||||
auto path_in_vendors = (vendor_path / bundle).replace_extension(".json");
|
||||
auto cache_in_rsrc = (rsrc_path / bundle).replace_extension(".opc");
|
||||
auto cache_in_vendors = (vendor_path / bundle).replace_extension(".opc");
|
||||
|
||||
if (!fs::exists(path_in_rsrc)) {
|
||||
// Either form of the vendor will do: a build may ship it as a cache alone.
|
||||
if (!fs::exists(path_in_rsrc) && !fs::exists(cache_in_rsrc)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Bundle not found in resources: " << bundle;
|
||||
return false;
|
||||
all_installed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create target directory if needed
|
||||
if (!fs::exists(vendor_path))
|
||||
fs::create_directories(vendor_path);
|
||||
|
||||
// Copy JSON file
|
||||
std::string error_message;
|
||||
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
|
||||
if (cfr != CopyFileResult::SUCCESS) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
|
||||
return false;
|
||||
bool installed_cache = false;
|
||||
if (installable_cache_version(rsrc_path, bundle).valid()) {
|
||||
installed_cache = copy_file(cache_in_rsrc.string(), cache_in_vendors.string(), error_message, false) == CopyFileResult::SUCCESS;
|
||||
if (! installed_cache) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Failed to copy " << bundle << ".opc: " << error_message;
|
||||
} else if (! VendorCacheFile::usable_version(cache_in_vendors.string(), bundle).valid()) {
|
||||
// The copy is what will be loaded, so it — not the kilobyte
|
||||
// peek that chose this form — decides whether the profile
|
||||
// beside it can go.
|
||||
BOOST_LOG_TRIVIAL(warning) << "Installed cache for " << bundle << " cannot be read; installing its profile instead";
|
||||
boost::system::error_code ec;
|
||||
fs::remove(cache_in_vendors, ec);
|
||||
installed_cache = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! installed_cache) {
|
||||
CopyFileResult cfr = copy_file(path_in_rsrc.string(), path_in_vendors.string(), error_message, false);
|
||||
if (cfr != CopyFileResult::SUCCESS) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to copy " << bundle << ".json: " << error_message;
|
||||
all_installed = false;
|
||||
continue;
|
||||
}
|
||||
// Only now: an earlier install's cache would shadow this profile,
|
||||
// but removing it before the profile lands would leave neither.
|
||||
boost::system::error_code ec;
|
||||
fs::remove(cache_in_vendors, ec);
|
||||
} else {
|
||||
// Left in place, an earlier install's profile would shadow the cache.
|
||||
boost::system::error_code ec;
|
||||
fs::remove(path_in_vendors, ec);
|
||||
if (ec)
|
||||
BOOST_LOG_TRIVIAL(warning) << "Could not remove the superseded profile " << path_in_vendors.string() << ": " << ec.message();
|
||||
}
|
||||
|
||||
// Copy the vendor directory (if it exists)
|
||||
auto dir_in_rsrc = rsrc_path / bundle;
|
||||
auto dir_in_vendors = vendor_path / bundle;
|
||||
|
||||
if (fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
|
||||
// Remove existing directory
|
||||
if (fs::exists(dir_in_vendors))
|
||||
fs::remove_all(dir_in_vendors);
|
||||
// Whatever is installed came from an earlier version of this vendor and
|
||||
// would be parsed in place of the one being installed now.
|
||||
if (fs::exists(dir_in_vendors))
|
||||
fs::remove_all(dir_in_vendors);
|
||||
|
||||
if (! installed_cache && fs::exists(dir_in_rsrc) && fs::is_directory(dir_in_rsrc)) {
|
||||
fs::create_directories(dir_in_vendors);
|
||||
|
||||
// Copy with file filter (same as PresetUpdater::install_bundles_rsrc)
|
||||
@@ -1787,11 +1915,11 @@ bool install_vendor_bundles_from_resources(
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Exception installing bundle " << bundle << ": " << e.what();
|
||||
return false;
|
||||
all_installed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return all_installed;
|
||||
}
|
||||
|
||||
void save_string_file(const boost::filesystem::path& p, const std::string& str)
|
||||
|
||||
Reference in New Issue
Block a user