fix merge conflicts

This commit is contained in:
Ian Chua
2026-07-15 21:04:42 +08:00
1025 changed files with 214936 additions and 22047 deletions
+20 -3
View File
@@ -304,6 +304,10 @@ void AppConfig::set_defaults()
if (get("show_3d_navigator").empty())
set_bool("show_3d_navigator", true);
// Show the one-time "Filament Track Switch is ready" tip until it has been seen once.
if (get("show_fila_switch_tips").empty())
set_bool("show_fila_switch_tips", true);
if (get("show_plate_gridlines").empty())
set_bool("show_plate_gridlines", true);
@@ -579,9 +583,16 @@ void AppConfig::set_defaults()
if (get("enable_step_mesh_setting").empty()) {
set_bool("enable_step_mesh_setting", true);
}
if (get("linear_defletion", "angle_defletion").empty()) {
set("linear_defletion", "0.003");
set("angle_defletion", "0.5");
// Migrate legacy misspelled keys (linear_defletion/angle_defletion) to the corrected spelling.
if (get("linear_deflection").empty() && !get("linear_defletion").empty())
set("linear_deflection", get("linear_defletion"));
if (get("angle_deflection").empty() && !get("angle_defletion").empty())
set("angle_deflection", get("angle_defletion"));
if (get("linear_deflection").empty()) {
set("linear_deflection", "0.003");
}
if (get("angle_deflection").empty()) {
set("angle_deflection", "0.5");
}
if (get("is_split_compound").empty()) {
set_bool("is_split_compound", false);
@@ -793,6 +804,10 @@ std::string AppConfig::load()
preset_info.nozzle_volume_type = NozzleVolumeType(cali_it.value()["nozzle_volume_type"].get<int>());
if (cali_it.value().contains("bed_type"))
preset_info.bed_type = BedType(cali_it.value()["bed_type"].get<int>());
if (cali_it.value().contains("nozzle_pos_id"))
preset_info.nozzle_pos_id = cali_it.value()["nozzle_pos_id"].get<int>();
if (cali_it.value().contains("nozzle_sn"))
preset_info.nozzle_sn = cali_it.value()["nozzle_sn"].get<std::string>();
cali_info.selected_presets.push_back(preset_info);
}
}
@@ -950,6 +965,8 @@ void AppConfig::save()
preset_json["extruder_id"] = filament_preset.extruder_id;
preset_json["nozzle_volume_type"] = int(filament_preset.nozzle_volume_type);
preset_json["bed_type"] = int(filament_preset.bed_type);
preset_json["nozzle_pos_id"] = filament_preset.nozzle_pos_id;
preset_json["nozzle_sn"] = filament_preset.nozzle_sn;
preset_json["nozzle_diameter"] = filament_preset.nozzle_diameter;
preset_json["filament_id"] = filament_preset.filament_id;
preset_json["setting_id"] = filament_preset.setting_id;
@@ -1660,7 +1660,7 @@ void SkeletalTrapezoidation::propagateBeadingsDownward(edge_t* edge_to_peak, ptr
}
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius)
{
assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0);
Beading ret = interpolate(left, ratio_left_to_whole, right);
@@ -1684,6 +1684,12 @@ SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beadin
{ // We cant adjust to fit the next edge because there is no previous one?!
return ret;
}
// ret follows the thicker of left/right, which can hold fewer insets than left when bead
// count and thickness disagree; skip the adjustment rather than index ret past its end.
if (next_inset_idx >= coord_t(ret.toolpath_locations.size()))
{
return ret;
}
assert(next_inset_idx < coord_t(left.toolpath_locations.size()));
assert(left.toolpath_locations[next_inset_idx] <= switching_radius);
assert(left.toolpath_locations[next_inset_idx + 1] >= switching_radius);
@@ -1703,7 +1709,7 @@ SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beadin
}
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const
SkeletalTrapezoidation::Beading SkeletalTrapezoidation::interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right)
{
assert(ratio_left_to_whole >= 0.0 && ratio_left_to_whole <= 1.0);
float ratio_right_to_whole = 1.0 - ratio_left_to_whole;
@@ -488,7 +488,7 @@ protected:
* beads.
* \return The beading at the interpolated location.
*/
Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const;
static Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius);
/*!
* Subroutine of \ref interpolate(const Beading&, Ratio, const Beading&, coord_t)
@@ -501,7 +501,7 @@ protected:
* \param right One of the beadings to interpolate between.
* \return The beading at the interpolated location.
*/
Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const;
static Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right);
/*!
* Get the beading at a certain node of the skeletal graph, or create one if
+19 -18
View File
@@ -61,7 +61,7 @@ static bool use_brim_efc_outline(const PrintObject &object)
&& object.config().raft_layers.value == 0;
}
//ORCA: Helper for snapping painted ears to the EFC outline.
//ORCA: Helper for projecting painted ears to the EFC outline.
static bool closest_point_on_expolygons(const ExPolygons &polygons, const Point &from, Point &closest_out)
{
double min_dist2 = std::numeric_limits<double>::max();
@@ -69,23 +69,22 @@ static bool closest_point_on_expolygons(const ExPolygons &polygons, const Point
for (const ExPolygon &poly : polygons) {
for (int i = 0; i < poly.num_contours(); ++i) {
const Point *candidate = poly.contour_or_hole(i).closest_point(from);
if (candidate == nullptr)
continue;
const int64_t dx = int64_t(candidate->x()) - int64_t(from.x());
const int64_t dy = int64_t(candidate->y()) - int64_t(from.y());
const double dist2 = double(dx * dx + dy * dy);
if (dist2 < min_dist2) {
min_dist2 = dist2;
closest_out = *candidate;
found = true;
const Lines lines = poly.contour_or_hole(i).lines();
for (const Line &line : lines) {
Point candidate;
const double dist2 = line.distance_to_squared(from, &candidate);
if (dist2 < min_dist2) {
min_dist2 = dist2;
closest_out = candidate;
found = true;
}
}
}
}
return found;
}
//ORCA: Helper for matching painted ears to their original island before EFC snapping.
//ORCA: Helper for matching painted ears to their original island before EFC projection.
static int find_containing_expolygon_index(const ExPolygons &polygons, const Point &from)
{
for (size_t idx = 0; idx < polygons.size(); ++idx) {
@@ -95,7 +94,7 @@ static int find_containing_expolygon_index(const ExPolygons &polygons, const Poi
return -1;
}
//ORCA: Keep painted ear snapping on the matching island when using EFC outline.
//ORCA: Keep painted ear projection on the matching island when using EFC outline.
static bool closest_point_on_matching_island(const ExPolygons &raw_outline, const ExPolygons &efc_outline, const Point &from, Point &closest_out)
{
const int island_idx = find_containing_expolygon_index(raw_outline, from);
@@ -106,6 +105,7 @@ static bool closest_point_on_matching_island(const ExPolygons &raw_outline, cons
}
return closest_point_on_expolygons(efc_outline, from, closest_out);
}
//ORCA: Use post-processed first-layer slices (including EFC) for brim outline.
// Returns ExPolygons of the bottom layer after all first-layer modifiers
// (including elephant foot compensation, if enabled) have been applied.
@@ -358,11 +358,12 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
if (brim_ear_points.size() <= 0) {
return mouse_ears_ex;
}
//ORCA: Painted ears can snap to the EFC-adjusted outline when enabled.
//ORCA: Painted ears follow the EFC-adjusted outline when enabled, while
// preserving their position along the selected outline segment.
const bool use_efc_outline = use_brim_efc_outline(*object);
const ExPolygons &raw_outline = object->layers().front()->lslices;
//ORCA: Lazily computed EFC-adjusted bottom outline.
//Stored separately so we can avoid recomputation unless EFC snapping is used.
//Stored separately so we can avoid recomputation unless EFC projection is used.
ExPolygons efc_outline_storage;
const ExPolygons* efc_outline = nullptr;
@@ -390,17 +391,17 @@ static ExPolygons make_brim_ears(const PrintObject* object, const double& flowWi
int32_t pt_x = scale_(pos.x());
int32_t pt_y = scale_(pos.y());
//ORCA: Snap painted ears to the EFC-adjusted outline when enabled.
//ORCA: Project painted ears to the EFC-adjusted outline when enabled.
if (use_efc_outline) {
if (efc_outline == nullptr) {
//ORCA: Compute EFC-adjusted outline lazily for painted ear snapping.
//ORCA: Compute the EFC-adjusted outline lazily for painted ear projection.
efc_outline_storage = get_print_object_bottom_layer_expolygons(*object);
efc_outline = &efc_outline_storage;
}
if (!efc_outline->empty()) {
Point closest_point;
//ORCA: Snap within the matching island to avoid drifting to another island.
//ORCA: Project within the matching island to avoid drifting to another island.
if (closest_point_on_matching_island(
raw_outline,
*efc_outline,
+3
View File
@@ -221,6 +221,7 @@ set(lisbslic3r_sources
GCode/FanMover.hpp
GCode/GCodeProcessor.cpp
GCode/GCodeProcessor.hpp
GCode/ElegooGCodeProcessorHelper.cpp
GCode.hpp
GCode/PchipInterpolatorHelper.cpp
GCode/PchipInterpolatorHelper.hpp
@@ -468,6 +469,8 @@ set(lisbslic3r_sources
FilamentGroup.cpp
FilamentGroupUtils.hpp
FilamentGroupUtils.cpp
MultiNozzleUtils.hpp
MultiNozzleUtils.cpp
GCode/ToolOrderUtils.hpp
GCode/ToolOrderUtils.cpp
FlushVolPredictor.hpp
+12
View File
@@ -188,6 +188,18 @@ ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta)
return results;
}
ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType)
{
Clipper2Lib::Paths64 subject = Slic3rExPolygons_to_Paths64(expolygons);
Clipper2Lib::ClipperOffset offsetter;
offsetter.AddPaths(subject, joinType, Clipper2Lib::EndType::Polygon);
Clipper2Lib::PolyPath64 polytree;
offsetter.Execute(delta, polytree);
ExPolygons results = PolyTreeToExPolygons(std::move(polytree));
return results;
}
ExPolygons offset2_ex_2(const ExPolygons& expolygons, double delta1, double delta2)
{
// 1st offset
+1
View File
@@ -15,6 +15,7 @@ Slic3r::Polylines diff_pl_2(const Slic3r::Polylines& subject, const Slic3r::Pol
ExPolygons union_ex_2(const Polygons &expolygons);
ExPolygons union_ex_2(const ExPolygons &expolygons);
ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta);
ExPolygons offset_ex_2(const ExPolygons &expolygons, double delta, Clipper2Lib::JoinType joinType);
ExPolygons offset2_ex_2(const ExPolygons &expolygons, double delta1, double delta2);
}
+75
View File
@@ -1,3 +1,7 @@
#include <limits>
#include <numeric>
#include <unordered_map>
#include "ClipperUtils.hpp"
#include "Geometry.hpp"
#include "ShortestPath.hpp"
@@ -930,6 +934,77 @@ Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r
Slic3r::Polylines intersection_pl(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip)
{ return _clipper_pl_closed(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip)); }
// Orca: Sort and orient open polyline fragments produced by clipping `source` with
// intersection_pl(), so that they run in the same order and direction as the source
// polyline. Clipping creates new endpoints at the clip boundary, but it keeps the
// interior source vertices intact, so a fragment's position on the source path is
// recovered exactly by looking its vertices up in the source. Fragments without any
// surviving source vertex lie on a single source segment, found by a nearest-segment
// search.
void restore_source_path_order(const Slic3r::Polyline &source, Slic3r::Polylines &fragments)
{
const Points &src = source.points;
if (src.size() < 2 || fragments.empty())
return;
std::unordered_map<Point, size_t, PointHash> source_index;
source_index.reserve(src.size());
for (size_t i = 0; i < src.size(); ++ i)
source_index.emplace(src[i], i);
// Sort key: index of the source vertex where the fragment starts, then the signed
// offset of the fragment's start from that vertex, to order multiple fragments cut
// from one long source segment.
std::vector<std::pair<size_t, double>> keys(fragments.size());
for (size_t n = 0; n < fragments.size(); ++ n) {
Polyline &pl = fragments[n];
const size_t npos = size_t(-1);
size_t front = npos;
size_t back = npos;
for (const Point &pt : pl.points)
if (auto it = source_index.find(pt); it != source_index.end()) {
front = it->second;
break;
}
for (auto i = pl.points.rbegin(); i != pl.points.rend(); ++ i)
if (auto it = source_index.find(*i); it != source_index.end()) {
back = it->second;
break;
}
Vec2crd source_dir;
if (front == npos) {
// All vertices were created by clipping, thus the whole fragment lies on a
// single source segment. Find that segment.
double best = std::numeric_limits<double>::max();
for (size_t i = 0; i + 1 < src.size(); ++ i)
if (double d = Line::distance_to_squared(pl.first_point(), src[i], src[i + 1]); d < best) {
best = d;
front = i;
}
back = front;
source_dir = src[front + 1] - src[front];
} else
source_dir = src[std::min(back + 1, src.size() - 1)] - src[front > 0 ? front - 1 : 0];
if (front > back) {
pl.reverse();
std::swap(front, back);
} else if (front == back &&
(pl.last_point() - pl.first_point()).cast<double>().dot(source_dir.cast<double>()) < 0.)
pl.reverse();
const Vec2crd seg = src[std::min(front + 1, src.size() - 1)] - src[front];
keys[n] = { front, (pl.first_point() - src[front]).cast<double>().dot(seg.cast<double>()) };
}
std::vector<size_t> order(fragments.size());
std::iota(order.begin(), order.end(), size_t(0));
std::sort(order.begin(), order.end(), [&keys](size_t a, size_t b) { return keys[a] < keys[b]; });
Polylines sorted;
sorted.reserve(fragments.size());
for (size_t n : order)
sorted.emplace_back(std::move(fragments[n]));
fragments = std::move(sorted);
}
Lines _clipper_ln(ClipperLib::ClipType clipType, const Lines &subject, const Polygons &clip)
{
// convert Lines to Polylines
+4
View File
@@ -528,6 +528,10 @@ Slic3r::Polylines intersection_pl(const Slic3r::Polygons &subject, const Slic3r
Slic3r::Polylines3 intersection_pl(const Slic3r::Polylines3 &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines3 intersection_pl(const Slic3r::Polylines3 &subject, const Slic3r::ExPolygon &clip);
// Orca: Sort and orient open polyline fragments produced by clipping `source` with
// intersection_pl(), so that they run in the same order and direction as the source polyline.
void restore_source_path_order(const Slic3r::Polyline &source, Slic3r::Polylines &fragments);
inline Slic3r::Lines intersection_ln(const Slic3r::Lines &subject, const Slic3r::Polygons &clip)
{
return _clipper_ln(ClipperLib::ctIntersection, subject, clip);
+30 -2
View File
@@ -364,6 +364,7 @@ public:
virtual void set_with_restore(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int stride) = 0;
virtual void set_with_restore_2(const ConfigOptionVectorBase* rhs, std::vector<int>& restore_index, int start, int len, bool skip_error = false) = 0;
virtual void set_only_diff(const ConfigOptionVectorBase* rhs, std::vector<int>& diff_index, int stride) = 0;
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector<int>& dest_index, int stride) = 0;
virtual void set_with_nil(const ConfigOptionVectorBase* rhs, const ConfigOptionVectorBase* inherits, int stride) = 0;
// Resize the vector of values, copy the newly added values from opt_default if provided.
virtual void resize(size_t n, const ConfigOption *opt_default = nullptr) = 0;
@@ -587,6 +588,32 @@ public:
throw ConfigurationError("ConfigOptionVector::set_only_diff(): Assigning an incompatible type");
}
//set a item related with extruder variants when apply static config with dynamic config
//rhs: item from dynamic config
//dest_index: which index in this vector need to be used
virtual void set_to_index(const ConfigOptionVectorBase* rhs, std::vector<int>& dest_index, int stride) override
{
if (rhs->type() == this->type()) {
// Assign the first value of the rhs vector.
auto other = static_cast<const ConfigOptionVector<T>*>(rhs);
T v = other->values.front();
this->values.resize(dest_index.size() * stride, v);
for (size_t i = 0; i < dest_index.size(); i++) {
if (dest_index[i] < 0)
continue;
for (size_t j = 0; j < size_t(stride); j++)
{
const size_t src_idx = size_t(dest_index[i]) * size_t(stride) + j;
if (src_idx < other->values.size() && !other->is_nil(size_t(dest_index[i]) * size_t(stride)))
this->values[i * size_t(stride) + j] = other->values[src_idx];
}
}
}
else
throw ConfigurationError("ConfigOptionVector::set_to_index(): Assigning an incompatible type");
}
//set a item related with extruder variants when saving user config, set the non-diff value of some extruder to nill
//this item has different value with inherit config
//rhs: item from userconfig
@@ -717,6 +744,7 @@ public:
return false;
}
// Apply an override option, possibly a nullable one.
//default_index are 0 based
bool apply_override(const ConfigOption *rhs, std::vector<int>& default_index) override {
if (this->nullable())
throw ConfigurationError("Cannot override a nullable ConfigOption.");
@@ -752,8 +780,8 @@ public:
this->values[i] = rhs_vec->values[i];
modified = true;
} else {
if ((i < default_index.size()) && (default_index[i] - 1 < default_value.size()))
this->values[i] = default_value[default_index[i] - 1];
if ((i < default_index.size()) && (default_index[i] < default_value.size()))
this->values[i] = default_value[default_index[i]];
else
this->values[i] = default_value[0];
}
+25 -7
View File
@@ -13,11 +13,20 @@ Extruder::Extruder(unsigned int id, GCodeConfig *config, bool share_extruder) :
{
reset();
m_config_index = int(m_id);
// cache values that are going to be called often
m_e_per_mm3 = this->filament_flow_ratio();
m_e_per_mm3 /= this->filament_crossection();
}
void Extruder::set_config_index(int idx)
{
m_config_index = idx < 0 ? int(m_id) : idx;
// keep the cached flow term reading the same column as the getters
m_e_per_mm3 = this->filament_flow_ratio();
m_e_per_mm3 /= this->filament_crossection();
}
unsigned int Extruder::extruder_id() const
{
assert(m_config);
@@ -162,28 +171,35 @@ double Extruder::filament_cost() const
double Extruder::filament_flow_ratio() const
{
return m_config->filament_flow_ratio.get_at(m_id);
return m_config->filament_flow_ratio.get_at(m_config_index);
}
// Return a "retract_before_wipe" percentage as a factor clamped to <0, 1>
double Extruder::retract_before_wipe() const
{
return std::min(1., std::max(0., m_config->retract_before_wipe.get_at(m_id) * 0.01));
return std::clamp(m_config->retract_before_wipe.get_at(m_config_index) * 0.01, 0., 1.);
}
// Orca:
// Return a "retract_after_wipe" percentage as a factor clamped to <0, 1>
double Extruder::retract_after_wipe() const
{
return std::min(std::clamp(m_config->retract_after_wipe.get_at(m_config_index) * 0.01, 0., 1.), 1. - retract_before_wipe());
}
double Extruder::retraction_length() const
{
return m_config->retraction_length.get_at(m_id);
return m_config->retraction_length.get_at(m_config_index);
}
double Extruder::retract_lift() const
{
return m_config->z_hop.get_at(m_id);
return m_config->z_hop.get_at(m_config_index);
}
int Extruder::retract_speed() const
{
return int(floor(m_config->retraction_speed.get_at(m_id)+0.5));
return int(floor(m_config->retraction_speed.get_at(m_config_index)+0.5));
}
bool Extruder::use_firmware_retraction() const
@@ -193,13 +209,13 @@ bool Extruder::use_firmware_retraction() const
int Extruder::deretract_speed() const
{
int speed = int(floor(m_config->deretraction_speed.get_at(m_id)+0.5));
int speed = int(floor(m_config->deretraction_speed.get_at(m_config_index)+0.5));
return (speed > 0) ? speed : this->retract_speed();
}
double Extruder::retract_restart_extra() const
{
return m_config->retract_restart_extra.get_at(m_id);
return m_config->retract_restart_extra.get_at(m_config_index);
}
double Extruder::retract_length_toolchange() const
@@ -214,6 +230,8 @@ double Extruder::retract_restart_extra_toolchange() const
double Extruder::travel_slope() const
{
// Orca: deliberately keyed by the physical extruder, not the filament column — this read
// predates the per-variant merge and switching it would change existing multi-extruder output.
return m_config->travel_slope.get_at(extruder_id()) * PI / 180;
}
+15
View File
@@ -29,6 +29,13 @@ public:
unsigned int id() const { return m_id; }
// Column of the per-variant filament/override arrays the getters read. Defaults to the
// filament id (one column per filament); the g-code generator refreshes it on layer changes
// and toolchanges when a per-layer nozzle grouping gives a filament several variant columns.
int config_index() const { return m_config_index; }
// idx < 0 resets to the filament id. Re-syncs the cached e_per_mm3 flow term.
void set_config_index(int idx);
unsigned int extruder_id() const;
double extrude(double dE);
double retract(double length, double restart_extra);
@@ -51,6 +58,10 @@ public:
double retracted() const { return m_retracted; }
// Get extra retraction planned after
double restart_extra() const { return m_restart_extra; }
// Share-aware retracted-length readers (for extruders shared between filaments), consumed by GCodeWriter::get_extruder_retracted_length.
bool is_share_extruder() const { return m_share_extruder; }
double get_single_retracted_length() const { return m_retracted; }
double get_share_retracted_length() const { return m_share_retracted[extruder_id()]; }
// Setters for the PlaceholderParser.
// Set current extruder position. Only applicable with absolute extruder addressing.
void set_position(double e) { m_E = e; }
@@ -63,6 +74,8 @@ public:
double filament_cost() const;
double filament_flow_ratio() const;
double retract_before_wipe() const;
// Orca:
double retract_after_wipe() const;
double retraction_length() const;
double retract_lift() const;
int retract_speed() const;
@@ -82,6 +95,8 @@ private:
GCodeConfig *m_config;
// Print-wide global ID of this extruder.
unsigned int m_id;
// Column into the per-variant filament/override arrays; equals m_id unless refreshed.
int m_config_index{0};
// Current state of the extruder axis, may be resetted if use_relative_e_distances.
double m_E;
// Current state of the extruder tachometer, used to output the extruded_volume() and used_filament() statistics.
+10 -1
View File
@@ -96,12 +96,21 @@ inline bool is_solid_infill(ExtrusionRole role)
|| role == erIroning;
}
inline bool is_bridge(ExtrusionRole role) {
inline bool is_bridge(ExtrusionRole role)
{
return role == erBridgeInfill
|| role == erInternalBridgeInfill
|| role == erOverhangPerimeter;
}
// Orca
inline bool is_support(ExtrusionRole role)
{
return role == erSupportMaterial
|| role == erSupportMaterialInterface
|| role == erSupportTransition;
}
class ExtrusionEntity
{
public:
File diff suppressed because it is too large Load Diff
+112 -46
View File
@@ -13,7 +13,8 @@
const static int DEFAULT_CLUSTER_SIZE = 16;
const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 5;
const static int ABSOLUTE_FLUSH_GAP_TOLERANCE = 10;
namespace Slic3r
{
@@ -52,12 +53,12 @@ namespace Slic3r
struct MemoryedGroup {
MemoryedGroup() = default;
MemoryedGroup(const std::vector<int>& group_, const int cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {}
MemoryedGroup(const std::vector<int>& group_, const double cost_, const int prefer_level_) :group(group_), cost(cost_), prefer_level(prefer_level_) {}
bool operator>(const MemoryedGroup& other) const {
return prefer_level < other.prefer_level || (prefer_level == other.prefer_level && cost > other.cost);
}
int cost{ 0 };
double cost{ 0 };
int prefer_level{ 0 };
std::vector<int>group;
};
@@ -75,6 +76,7 @@ namespace Slic3r
std::vector<FilamentGroupUtils::FilamentInfo> filament_info;
std::vector<std::string> filament_ids;
std::vector<std::set<int>> unprintable_filaments;
std::map<int, std::set<NozzleVolumeType>> unprintable_volumes;
} model_info;
struct GroupInfo {
@@ -82,40 +84,74 @@ namespace Slic3r
double max_gap_threshold;
FGMode mode;
FGStrategy strategy;
bool ignore_ext_filament; //wai gua filament
bool ignore_ext_filament;
bool has_filament_switcher = false;
std::vector<int> filament_volume_map;
} group_info;
struct MachineInfo {
std::vector<int> max_group_size;
std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>> machine_filament_info;
std::vector<std::pair<std::set<int>, int>> extruder_group_size;
std::vector<bool> prefer_non_model_filament;
int master_extruder_id;
} machine_info;
struct SpeedInfo{
std::unordered_map<int,std::unordered_map<int,double>> filament_print_time;
double extruder_change_time;
double filament_change_time;
bool group_with_time;
MultiNozzleUtils::FilamentChangeTimeParams change_time_params;
std::vector<bool> ams_preload_enabled;
} speed_info;
struct NozzleInfo {
std::map<int, std::vector<int>> extruder_nozzle_list;
std::vector<MultiNozzleUtils::NozzleInfo> nozzle_list;
std::unordered_map<int, int> nozzle_status;
} nozzle_info;
};
std::vector<int> select_best_group_for_ams(const std::vector<std::vector<int>>& map_lists,
std::vector<int> select_best_group_for_ams(const std::vector<std::vector<int>> &filament_to_nozzles,
const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments,
const std::vector<FilamentGroupUtils::FilamentInfo>& used_filament_info,
const std::vector<std::vector<FilamentGroupUtils::MachineFilamentInfo>>& machine_filament_info,
const bool has_filament_switcher = false,
const double color_delta_threshold = 20);
std::vector<int> optimize_group_for_master_extruder(const std::vector<unsigned int>& used_filaments, const FilamentGroupContext& ctx, const std::vector<int>& filament_map);
bool can_swap_groups(const int extruder_id_0, const std::set<int>& group_0, const int extruder_id_1, const std::set<int>& group_1, const FilamentGroupContext& ctx);
std::vector<int> calc_filament_group_for_tpu(const std::set<int>& tpu_filaments, const int filament_nums, const int master_extruder_id);
class FlushDistanceEvaluator
{
public:
FlushDistanceEvaluator(const FlushMatrix& flush_matrix,const std::vector<unsigned int>&used_filaments,const std::vector<std::vector<unsigned int>>& layer_filaments, double p = 0.65);
FlushDistanceEvaluator(const std::vector<FlushMatrix>& flush_matrix,const std::vector<unsigned int>&used_filaments,const std::vector<std::vector<unsigned int>>& layer_filaments, double p = 0.65);
~FlushDistanceEvaluator() = default;
double get_distance(int idx_a, int idx_b) const;
double get_distance(int idx_a, int idx_b, int extruder_id) const;
private:
std::vector<std::vector<float>>m_distance_matrix;
std::vector<std::vector<std::vector<float>>>m_distance_matrix;
};
class TimeEvaluator
{
public:
TimeEvaluator(const FilamentGroupContext::SpeedInfo& speed_info) : m_speed_info(speed_info) {}
double get_estimated_time(const std::vector<int>& filament_map) const;
private:
FilamentGroupContext::SpeedInfo m_speed_info;
};
// Search budget for the k-medoids clustering, an anytime search. Each restart is seeded from its
// own index, so what it returns depends on how many restarts complete before the clock expires,
// and therefore on the speed of the machine. A timeout_ms <= 0 removes the clock and bounds the
// search by max_restarts alone.
struct ClusteringBudget
{
int timeout_ms = 3000;
int max_restarts = 30;
};
class FilamentGroup
{
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
@@ -123,17 +159,25 @@ namespace Slic3r
public:
explicit FilamentGroup(const FilamentGroupContext& ctx_) :ctx(ctx_) {}
public:
void set_clustering_budget(const ClusteringBudget& budget) { m_clustering_budget = budget; }
std::vector<int> calc_filament_group(int * cost = nullptr);
std::vector<std::vector<int>> get_memoryed_groups()const { return m_memoryed_groups; }
public:
std::vector<int> calc_filament_group_for_match(int* cost = nullptr);
std::vector<int> calc_filament_group_for_flush(int* cost = nullptr);
std::vector<int> calc_filament_group_for_tpu(int* cost = nullptr);
private:
std::vector<int> calc_min_flush_group(int* cost = nullptr);
std::vector<int> calc_min_flush_group_by_enum(const std::vector<unsigned int>& used_filaments, int* cost = nullptr);
std::vector<int> calc_min_flush_group_by_pam2(const std::vector<unsigned int>& used_filaments, int* cost = nullptr, int timeout_ms = 300);
std::vector<int> calc_group_by_enum(int k, const std::vector<unsigned int>& used_filaments,
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr);
std::vector<int> calc_group_by_kmedoids(int k, const std::vector<unsigned int>& used_filaments,
const std::unordered_map<int, std::vector<int>>& unplaceable_limits, int* cost = nullptr);
std::map<int, int> rebuild_unprintables(const std::vector<unsigned int>& used_filaments, const std::map<int,int>& extruder_unprintables);
std::unordered_map<int, std::vector<int>> rebuild_nozzle_unprintables(const std::vector<unsigned int>& used_filaments, const std::unordered_map<int, std::vector<int>>& extruder_unprintables, const std::vector<int>& filament_volume_map);
std::unordered_map<int, std::vector<int>> try_merge_filaments();
void rebuild_context(const std::unordered_map<int, std::vector<int>>& merged_filaments);
@@ -141,57 +185,79 @@ namespace Slic3r
private:
FilamentGroupContext ctx;
MemoryedGroupHeap m_memoryed_heap;
std::vector<std::vector<int>> m_memoryed_groups;
ClusteringBudget m_clustering_budget;
public:
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq;
};
class KMediods2
std::vector<int> calc_filament_group_for_manual_multi_nozzle(const std::vector<int>& filament_map_manual,const FilamentGroupContext& ctx);
std::vector<int> calc_filament_group_for_match_multi_nozzle(const FilamentGroupContext& ctx);
struct FilamentPlanRes
{
std::vector<int> fil_order;
std::vector<int> fil_nozzle_match;
};
std::vector<FilamentPlanRes> plan_filament_nozzle_mapping_and_order(const FilamentGroupContext& ctx);
class KMediods
{
protected:
using MemoryedGroupHeap = FilamentGroupUtils::MemoryedGroupHeap;
using MemoryedGroup = FilamentGroupUtils::MemoryedGroup;
enum INIT_TYPE
{
Random = 0,
Farthest
};
public:
KMediods2(const int elem_count, const std::shared_ptr<FlushDistanceEvaluator>& evaluator, int default_group_id = 0) :
m_evaluator{ evaluator },
m_elem_count{ elem_count },
m_default_group_id{ default_group_id }
{
m_max_cluster_size = std::vector<int>(m_k, DEFAULT_CLUSTER_SIZE);
KMediods(const int k, const int elem_count, const std::shared_ptr<FlushDistanceEvaluator>& evaluator, int default_group_id = 0) {
m_k = k;
m_evaluator = evaluator;
m_max_cluster_size = std::vector<int>(k, DEFAULT_CLUSTER_SIZE);
m_elem_count = elem_count;
m_default_group_id = default_group_id;
}
// set max group size
void set_max_cluster_size(const std::vector<int>& group_size) { m_max_cluster_size = group_size; }
// key stores elem idx, value stores the cluster id that elem cnanot be placed
void set_unplaceable_limits(const std::map<int, int>& placeable_limits) { m_unplaceable_limits = placeable_limits; }
void set_cluster_group_size(const std::vector<std::pair<std::set<int>,int>>& cluster_group_size);
void do_clustering(const FGStrategy& g_strategy,int timeout_ms = 100);
// key stores elem, value stores the cluster id that the elem must be placed
void set_placable_limits(const std::unordered_map<int, std::vector<int>>& placable_limits) { m_placeable_limits = placable_limits; }
// key stores elem, value stores the cluster id that the elem cannot be placed
void set_unplacable_limits(const std::unordered_map<int, std::vector<int>>& unplacable_limits) { m_unplaceable_limits = unplacable_limits; }
void set_memory_threshold(double threshold) { memory_threshold = threshold; }
MemoryedGroupHeap get_memoryed_groups()const { return memoryed_groups; }
std::vector<int>get_cluster_labels()const { return m_cluster_labels; }
void do_clustering(const FilamentGroupContext& context, const ClusteringBudget& budget);
std::vector<int> get_cluster_labels()const { return m_cluster_labels; }
private:
std::vector<int>cluster_small_data(const std::map<int, int>& unplaceable_limits, const std::vector<int>& group_size);
std::vector<int>assign_cluster_label(const std::vector<int>& center, const std::map<int, int>& unplaceable_limits, const std::vector<int>& group_size, const FGStrategy& strategy);
int calc_cost(const std::vector<int>& labels, const std::vector<int>& medoids);
protected:
FilamentGroupUtils::MemoryedGroupHeap memoryed_groups;
std::shared_ptr<FlushDistanceEvaluator> m_evaluator;
std::map<int, int>m_unplaceable_limits;
std::vector<int>m_cluster_labels;
std::vector<int>m_max_cluster_size;
bool have_enough_size(const std::vector<int>& cluster_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size,int elem_count);
// calculate cluster distance
int calc_cost(const std::vector<int>& clusters, const std::vector<int>& cluster_centers, int cluster_id = -1);
const int m_k = 2;
// get initial cluster center
std::vector<int>init_cluster_center(const std::unordered_map<int, std::vector<int>>& placeable_limits, const std::unordered_map<int, std::vector<int>>& unplaceable_limits, const std::vector<int>& cluster_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size, int seed);
// assign each elem to the cluster
std::vector<int> assign_cluster_label(const std::vector<int>& center, const std::unordered_map<int, std::vector<int>>& placeable_limits, const std::unordered_map<int, std::vector<int>>& unplaceable_limits, const std::vector<int>& group_size, const std::vector<std::pair<std::set<int>, int>>& cluster_group_size);
protected:
MemoryedGroupHeap memoryed_groups;
std::shared_ptr<FlushDistanceEvaluator>m_evaluator;
std::unordered_map<int, std::vector<int>> m_unplaceable_limits; // key: filament, value: nozzle ids it cannot be assigned to
std::unordered_map<int, std::vector<int>> m_placeable_limits; // key: filament, value: nozzle ids it must be assigned to
std::vector<int>m_max_cluster_size; // max number of filaments each nozzle can hold
std::vector<int>m_cluster_labels; // assignment result, resolved down to nozzle id
std::vector<std::pair<std::set<int>,int>> m_cluster_group_size;
std::vector<int> m_nozzle_to_extruder;
int m_k;
int m_elem_count;
int m_default_group_id{ 0 };
double memory_threshold{ 0 };
+65
View File
@@ -274,5 +274,70 @@ namespace FilamentGroupUtils
}
return true;
}
int get_estimate_extruder_change_count(const std::vector<std::vector<unsigned int>> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
{
int ret = 0;
for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) {
int extruder_count = extruder_nozzle_info.get_used_extruders(layer_id).size();
ret += (extruder_count - 1);
}
return ret;
}
int get_estimate_nozzle_change_count(const std::vector<std::vector<unsigned int>> &layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
{
int ret = 0;
for (size_t layer_id = 0; layer_id < layer_filaments.size(); ++layer_id) {
auto extruder_list = extruder_nozzle_info.get_used_extruders(layer_id);
for (auto extruder_id : extruder_list) {
int nozzle_count = extruder_nozzle_info.get_used_nozzles_in_extruder(extruder_id, layer_id).size();
if (nozzle_count > 1) ret += (nozzle_count - 1);
}
}
return ret;
}
std::pair<int, int> get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult &extruder_nozzle_info)
{
std::pair<int, int> ret{0,0};
int layer_nums = extruder_nozzle_info.get_layer_filament_sequences().size();
for (int layer_id = 0; layer_id < layer_nums; layer_id++) {
std::vector<int> extruders = extruder_nozzle_info.get_used_extruders(layer_id);
ret.first = extruders.size() - 1;
for (auto ext_id : extruders) {
int nozzles = extruder_nozzle_info.get_used_nozzles_in_extruder(ext_id, layer_id).size();
ret.second += nozzles;
}
ret.second = std::max(0, ret.second - ret.first);
}
return ret;
}
std::map<int,std::vector<int>> build_extruder_nozzle_list(const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list)
{
std::map<int, std::vector<int>> ret;
for (auto& nozzle : nozzle_list) {
ret[nozzle.extruder_id].emplace_back(nozzle.group_id);
}
for (auto& elem : ret)
std::sort(elem.second.begin(), elem.second.end());
return ret;
}
std::vector<int> update_used_filament_values(const std::vector<int>& old_values, const std::vector<int>& new_values, const std::vector<unsigned int>& used_filaments)
{
std::vector<int> res = old_values;
for (size_t i = 0; i < used_filaments.size(); ++i) {
// Orca: guard against filament ids beyond the map sizes (possible with
// mis-normalized per-filament arrays from CLI inputs); skip instead of UB.
if (used_filaments[i] >= res.size() || used_filaments[i] >= new_values.size())
continue;
res[used_filaments[i]] = new_values[used_filaments[i]];
}
return res;
}
}
}
+19
View File
@@ -7,6 +7,7 @@
#include <exception>
#include "PrintConfig.hpp"
#include "MultiNozzleUtils.hpp"
namespace Slic3r
{
@@ -31,6 +32,10 @@ namespace Slic3r
Color color;
std::string type;
bool is_support;
// How this filament is used across the model. Orca's shipping grouping
// algorithm does not read it yet; defaulted so a default-built FilamentInfo
// is deterministic. The nozzle-centric engine consumes it later.
FilamentUsageType usage_type = FilamentUsageType::ModelOnly;
};
struct MachineFilamentInfo: public FilamentInfo {
@@ -80,6 +85,20 @@ namespace Slic3r
void extract_unprintable_limit_indices(const std::vector<std::set<int>>& unprintable_elems, const std::vector<unsigned int>& used_filaments, std::unordered_map<int, std::vector<int>>& unplaceable_limits);
bool check_printable(const std::vector<std::set<int>>& groups, const std::map<int, int>& unprintable);
// Nozzle-centric grouping helpers. The estimate helpers read a LayeredNozzleGroupResult's
// per-layer extruder/nozzle usage; the two builders support building the grouping context
// (extruder->nozzle inventory) and writing back a resolved map onto only the used-filament
// slots.
int get_estimate_extruder_change_count(const std::vector<std::vector<unsigned int>>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
int get_estimate_nozzle_change_count(const std::vector<std::vector<unsigned int>>& layer_filaments, const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
std::pair<int, int> get_estimate_extruder_filament_change_count(const MultiNozzleUtils::LayeredNozzleGroupResult& extruder_nozzle_info);
std::map<int, std::vector<int>> build_extruder_nozzle_list(const std::vector<MultiNozzleUtils::NozzleInfo>& nozzle_list);
std::vector<int> update_used_filament_values(const std::vector<int>& old_values, const std::vector<int>& new_values, const std::vector<unsigned int>& used_filaments);
}
+91 -12
View File
@@ -272,6 +272,12 @@ struct SurfaceFillParams
// For Gyroid: when true, use the parameterized "optimized" wave.
bool gyroid_optimized = false;
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
bool separated_infills{false};
// Orca: forced print order of surface fill loops/fragments for center-based patterns.
SurfaceFillOrder fill_order = SurfaceFillOrder::Default;
bool operator<(const SurfaceFillParams &rhs) const {
#define RETURN_COMPARE_NON_EQUAL(KEY) if (this->KEY < rhs.KEY) return true; if (this->KEY > rhs.KEY) return false;
#define RETURN_COMPARE_NON_EQUAL_TYPED(TYPE, KEY) if (TYPE(this->KEY) < TYPE(rhs.KEY)) return true; if (TYPE(this->KEY) > TYPE(rhs.KEY)) return false;
@@ -301,8 +307,12 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(lateral_lattice_angle_2);
RETURN_COMPARE_NON_EQUAL(symmetric_infill_y_axis);
RETURN_COMPARE_NON_EQUAL(infill_lock_depth);
RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
RETURN_COMPARE_NON_EQUAL_TYPED(unsigned, fill_order);
return false;
}
@@ -329,7 +339,10 @@ struct SurfaceFillParams
this->infill_lock_depth == rhs.infill_lock_depth &&
this->skin_infill_depth == rhs.skin_infill_depth &&
this->infill_overhang_angle == rhs.infill_overhang_angle &&
this->gyroid_optimized == rhs.gyroid_optimized;
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
this->fill_order == rhs.fill_order;
}
};
@@ -868,6 +881,8 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.lateral_lattice_angle_1 = region_config.lateral_lattice_angle_1;
params.lateral_lattice_angle_2 = region_config.lateral_lattice_angle_2;
params.infill_overhang_angle = region_config.infill_overhang_angle;
params.center_of_surface_pattern = region_config.center_of_surface_pattern;
params.separated_infills = region_config.separated_infills;
if (params.pattern == ipLockedZag) {
params.infill_lock_depth = scale_(region_config.infill_lock_depth);
params.skin_infill_depth = scale_(region_config.skin_infill_depth);
@@ -922,6 +937,14 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extruder = region_config.bottom_surface_filament_id;
else if (params.extrusion_role == erSolidInfill)
params.extruder = region_config.internal_solid_filament_id;
// Orca: forced fill order applies only to top/bottom surfaces filled with a
// center-based pattern; everything else stays at Default to keep batching together.
if (params.pattern == ipConcentric || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
if (params.extrusion_role == erTopSolidInfill)
params.fill_order = region_config.top_surface_fill_order.value;
else if (params.extrusion_role == erBottomSurface)
params.fill_order = region_config.bottom_surface_fill_order.value;
}
// Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
@@ -936,9 +959,16 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
region_config.sparse_infill_rotate_template.value);
params.fixed_angle = !region_config.sparse_infill_rotate_template.value.empty();
} else {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.solid_infill_direction.value,
region_config.solid_infill_rotate_template.value);
params.fixed_angle = !region_config.solid_infill_rotate_template.value.empty();
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.;
if (top_layer_direction_set || bottom_layer_direction_set) {
params.angle = Geometry::deg2rad(top_layer_direction_set ? region_config.top_layer_direction.value : region_config.bottom_layer_direction.value);
params.fixed_angle = true;
} else {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.solid_infill_direction.value,
region_config.solid_infill_rotate_template.value);
params.fixed_angle = !region_config.solid_infill_rotate_template.value.empty();
}
}
params.bridge_angle = float(surface.bridge_angle);
@@ -1301,6 +1331,22 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
auto &region_config = layerm->region().config();
params.config = &region_config;
params.pattern = surface_fill.params.pattern;
params.fill_order = surface_fill.params.fill_order;
// Orca: Checking the filling of a centered surface by drawing for each model parts
bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral;
if (is_top_or_bottom) {
params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern
}
// Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly
// fall through to the default (whole-object) bounding box below.
bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill;
bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills &&
(
is_separable_infill_pattern(surface_fill.params.pattern) ||
params.config->solid_infill_rotate_template != "" ||
params.config->sparse_infill_rotate_template != "" );
if( surface_fill.params.pattern == ipLockedZag ) {
params.locked_zag = true;
@@ -1325,7 +1371,36 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.can_reverse = false;
for (ExPolygon& expoly : surface_fill.expolygons) {
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
// Orca: separate infill / per-model pattern centering.
//
// Center the pattern on each connected body of the object independently, so every piece
// is filled exactly as if it were sliced on its own: touching/overlapping parts merge
// into one body sharing a center, while separate parts and disconnected islands (even
// interleaved-but-not-touching ones, e.g. chain links) each get their own. The body each
// island belongs to, and its full bounding box, were resolved in 3D by PrintObject::
// infill() (lslices_separated_component_bboxes, aligned with this layer's lslices). We
// match this fill region to the island it overlaps most, then re-use the whole-object
// bounding box (origin-centered — identical extent to the default, so coverage and cost
// are unchanged) re-centered on that body.
if (is_per_model_center || is_separate_infill) {
double best_overlap = 0.;
BoundingBox best_component;
for (size_t r = 0; r < this->lslices.size() && r < this->lslices_separated_component_bboxes.size(); ++ r) {
const double overlap = area(intersection_ex(this->lslices[r], expoly));
if (overlap > best_overlap) {
best_overlap = overlap;
best_component = this->lslices_separated_component_bboxes[r];
}
}
if (best_component.defined) {
const Point c = best_component.center();
BoundingBox part_bbox = bbox; // origin-centered, whole-object extent (from above)
part_bbox.translate(c.x(), c.y()); // re-center on this body
f->set_bounding_box(part_bbox);
}
} // - End: separate infill / per-model pattern centering
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis);
@@ -1599,16 +1674,20 @@ void Layer::make_ironing()
ironing_params.height = default_layer_height * 0.01 * (!config.filament_ironing_flow.is_nil(extruder_idx)
? config.filament_ironing_flow.get_at(extruder_idx)
: config.ironing_flow);
ironing_params.speed = (!config.filament_ironing_speed.is_nil(extruder_idx)
? config.filament_ironing_speed.get_at(extruder_idx)
: config.ironing_speed);
double ironing_angle = (config.ironing_angle_fixed ? 0 : calculate_infill_rotation_angle(this->object(), this->id(), config.solid_infill_direction.value, config.solid_infill_rotate_template.value)) + config.ironing_angle * M_PI / 180.;
ironing_params.speed = (!config.filament_ironing_speed.is_nil(extruder_idx)
? config.filament_ironing_speed.get_at(extruder_idx)
: config.ironing_speed);
const bool top_layer_direction_set = config.top_layer_direction.value >= 0.;
const double top_layer_base_angle = top_layer_direction_set ?
Geometry::deg2rad(config.top_layer_direction.value) :
calculate_infill_rotation_angle(this->object(), this->id(), config.solid_infill_direction.value, config.solid_infill_rotate_template.value);
double ironing_angle = (config.ironing_angle_fixed ? 0. : top_layer_base_angle) + config.ironing_angle * M_PI / 180.;
if (config.align_infill_direction_to_model) {
auto m = this->object()->trafo().matrix();
ironing_angle += atan2((double)m(1, 0), (double)m(0, 0));
}
ironing_params.angle = ironing_angle;
ironing_params.fixed_angle = config.ironing_angle_fixed || !config.solid_infill_rotate_template.value.empty();
ironing_params.angle = ironing_angle;
ironing_params.fixed_angle = config.ironing_angle_fixed || top_layer_direction_set || !config.solid_infill_rotate_template.value.empty();
ironing_params.pattern = config.ironing_pattern;
ironing_params.layerm = layerm;
by_extruder.emplace_back(ironing_params);
+6 -5
View File
@@ -162,10 +162,11 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
out.push_back(eec = new ExtrusionEntityCollection());
// Only concentric fills are not sorted.
eec->no_sort = this->no_sort();
// ORCA: special flag for flow rate calibration
auto is_flow_calib = params.extrusion_role == erTopSolidInfill && this->print_object_config->has("calib_flowrate_topinfill_special_order") &&
this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool();
if (is_flow_calib) {
// Orca: a forced surface fill order must survive the G-code path planner, which would
// otherwise re-chain and possibly reverse the paths. This also covers the flow rate
// calibration, which forces an outward fill order on its top surfaces.
const bool keep_fill_order = params.fill_order != SurfaceFillOrder::Default;
if (keep_fill_order) {
eec->no_sort = true;
}
size_t idx = eec->entities.size();
@@ -180,7 +181,7 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
params.extrusion_role,
flow_mm3_per_mm, float(flow_width), params.flow.height());
}
if (!params.can_reverse || is_flow_calib) {
if (!params.can_reverse || keep_fill_order) {
for (size_t i = idx; i < eec->entities.size(); i++)
eec->entities[i]->set_reverse();
}
+5
View File
@@ -100,12 +100,17 @@ struct FillParams
bool dont_sort{ false }; // do not sort the lines, just simply connect them
bool can_reverse{true};
// Orca: forced print order of surface fill loops/fragments for center-based patterns
// (Concentric, Archimedean Chords, Octagram Spiral). Default keeps shortest-path ordering.
SurfaceFillOrder fill_order { SurfaceFillOrder::Default };
float horiz_move{0.0}; //move infill to get cross zag pattern
bool symmetric_infill_y_axis{false};
coord_t symmetric_y_axis{0};
bool locked_zag{false};
float infill_lock_depth{0.0};
float skin_infill_depth{0.0};
CenterOfSurfacePattern center_of_surface_pattern{CenterOfSurfacePattern::Each_Surface};
};
static_assert(IsTriviallyCopyable<FillParams>::value, "FillParams class is not POD (and it should be - see constructor).");
+17 -1
View File
@@ -41,6 +41,10 @@ void FillConcentric::_fill_surface_single(
// 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);
// Orca: an outward fill order prints the innermost loops first instead.
if (params.fill_order == SurfaceFillOrder::Outward)
std::reverse(loops.begin(), loops.end());
// split paths using a nearest neighbor search
size_t iPathFirst = polylines_out.size();
@@ -108,6 +112,17 @@ void FillConcentric::_fill_surface_single(const FillParams& params,
all_extrusions.emplace_back(&wall);
}
// Orca: a forced fill order prints the loops in strictly monotonic depth order so
// that surfaces broken up by holes or slots cannot hop outward and back inward.
const bool forced_fill_order = params.fill_order != SurfaceFillOrder::Default;
if (forced_fill_order) {
const bool outward = params.fill_order == SurfaceFillOrder::Outward;
std::stable_sort(all_extrusions.begin(), all_extrusions.end(),
[outward](const Arachne::ExtrusionLine *a, const Arachne::ExtrusionLine *b) {
return outward ? a->inset_idx > b->inset_idx : a->inset_idx < b->inset_idx;
});
}
// Split paths using a nearest neighbor search.
size_t firts_poly_idx = thick_polylines_out.size();
Point last_pos(0, 0);
@@ -136,7 +151,8 @@ void FillConcentric::_fill_surface_single(const FillParams& params,
if (j < thick_polylines_out.size())
thick_polylines_out.erase(thick_polylines_out.begin() + int(j), thick_polylines_out.end());
reorder_by_shortest_traverse(thick_polylines_out);
if (!forced_fill_order)
reorder_by_shortest_traverse(thick_polylines_out);
}
else {
Polylines polylines;
+28 -33
View File
@@ -77,20 +77,24 @@ void FillPlanePath::_fill_surface_single(
//FIXME Vojtech: We are not sure whether the user expects the fill patterns on visible surfaces to be aligned across all the islands of a single layer.
// One may align for this->centered() to align the patterns for Archimedean Chords and Octagram Spiral patterns.
const bool align = params.density < 0.995;
// Orca: the old implementation became obsolete when it became possible to change the density of the top and bottom surfaces
bool align = params.extrusion_role == ExtrusionRole::erInternalInfill;
BoundingBox bounding_box;
BoundingBox snug_bounding_box = get_extents(expolygon).inflated(SCALED_EPSILON);
// Expand the bounding box to avoid artifacts at the edges
snug_bounding_box.offset(scale_(this->spacing)*params.multiline);
snug_bounding_box.offset(scale_(this->spacing)*params.multiline);
// Rotated bounding box of the area to fill in with the pattern.
BoundingBox bounding_box = align ?
// Sparse infill needs to be aligned across layers. Align infill across layers using the object's bounding box.
this->bounding_box.rotated(-direction.first) :
// Solid infill does not need to be aligned across layers, generate the infill pattern
// around the clipping expolygon only.
snug_bounding_box;
// Sparse infill (or Internal where align == true) needs to be aligned across layers. Align infill across layers using the object's bounding box.
// Solid infill does not need to be aligned across layers, generate the infill pattern around the clipping expolygon only.
if (align)
bounding_box = this->bounding_box.rotated(-direction.first);
else if (params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Surface)
bounding_box = snug_bounding_box;
else if (params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model)
bounding_box = this->bounding_box.rotated(-direction.first);
else
bounding_box = extended_object_bounding_box();
Point shift = this->centered() ?
bounding_box.center() :
@@ -130,31 +134,22 @@ void FillPlanePath::_fill_surface_single(
if (!polylines.empty()) {
Polylines chained;
if (params.dont_connect() || params.density > 0.5) {
// ORCA: special flag for flow rate calibration
auto is_flow_calib = params.extrusion_role == erTopSolidInfill &&
this->print_object_config->has("calib_flowrate_topinfill_special_order") &&
this->print_object_config->option("calib_flowrate_topinfill_special_order")->getBool() &&
dynamic_cast<FillArchimedeanChords*>(this);
if (is_flow_calib) {
// We want the spiral part to be printed inside-out
// Find the center spiral line first, by looking for the longest one
auto it = std::max_element(polylines.begin(), polylines.end(),
[](const Polyline& a, const Polyline& b) { return a.length() < b.length(); });
Polyline center_spiral = std::move(*it);
// Ensure the spiral is printed from inside to out
if (center_spiral.first_point().squaredNorm() > center_spiral.last_point().squaredNorm()) {
center_spiral.reverse();
if (params.fill_order != SurfaceFillOrder::Default) {
// Orca: print the fragments in the order they appear along the generated
// path, which runs from the center outwards. The Euclidean distance from
// the center cannot be used for this: along the Octagram Spiral the radius
// oscillates by far more than the ring spacing, so fragments of different
// rings would interleave.
restore_source_path_order(polyline, polylines);
chained = std::move(polylines);
if (params.fill_order == SurfaceFillOrder::Inward) {
// The source path runs from the center outwards; flip everything for inward.
std::reverse(chained.begin(), chained.end());
for (Polyline &pl : chained)
pl.reverse();
}
// Chain the other polylines
polylines.erase(it);
chained = chain_polylines(std::move(polylines));
// Then add the center spiral back
chained.push_back(std::move(center_spiral));
} else {
chained = chain_polylines(std::move(polylines));
chained = chain_polylines(std::move(polylines), nullptr);
}
} else
connect_infill(std::move(polylines), expolygon, chained, this->spacing, params);
+31 -8
View File
@@ -2739,13 +2739,19 @@ static void polylines_from_paths(const std::vector<MonotonicRegionLink> &path, c
// The extended bounding box of the whole object that covers any rotation of every layer.
BoundingBox FillRectilinear::extended_object_bounding_box() const {
// Build the extension around the box center. The transpose merge and the sqrt(2.) scaling
// (which covers any possible rotation) are both defined about the origin, so a box that is not
// origin-centered — e.g. a separated-infill box re-centered on a single assembly part — would be
// distorted. Shift to the origin first and back afterwards; for the default origin-centered box
// the two translations cancel and this is identical to the original behavior.
const Point c = this->bounding_box.center();
BoundingBox out = this->bounding_box;
out.translate(-c.x(), -c.y());
out.merge(Point(out.min.y(), out.min.x()));
out.merge(Point(out.max.y(), out.max.x()));
// The bounding box is scaled by sqrt(2.) to ensure that the bounding box
// covers any possible rotations.
return out.scaled(sqrt(2.));
out = out.scaled(sqrt(2.));
out.translate(c.x(), c.y());
return out;
}
bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillParams &params, float angleBase, float pattern_shift, Polylines &polylines_out)
@@ -3098,8 +3104,11 @@ bool FillRectilinear::fill_surface_trapezoidal(
const coord_t d2 = coord_t(0.5 * period - d1);
// Align bounding box to the grid
bb.merge(align_to_grid(bb.min, Point(period, period)));
// Align bounding box to the grid, phased through the box center so separated infills align
// each part on itself (grid_center is the origin for a standalone object / feature off).
// Captured before the merge, which grows bb and would otherwise shift its center.
const Point grid_center = bb.center();
bb.merge(align_to_grid(bb.min, Point(period, period), grid_center));
const coord_t xmin = bb.min.x();
const coord_t xmax = bb.max.x();
const coord_t ymin = bb.min.y();
@@ -3146,11 +3155,17 @@ bool FillRectilinear::fill_surface_trapezoidal(
flip_vertical = !flip_vertical;
}
// transpose points for odd infill layers (taking infill combination into account)
// transpose points for odd infill layers (taking infill combination into account).
// Orca: mirror across the diagonal through grid_center (not the origin), so the swapped
// layers stay aligned with the center-phased grid. For a standalone object / feature off,
// grid_center is the origin and this is a plain x/y swap.
if (infill_layer_id % 2 == 1) {
for (Polyline& pl : polylines) {
for (Point& p : pl.points) {
std::swap(p.x(), p.y());
const coord_t dx = p.x() - grid_center.x();
const coord_t dy = p.y() - grid_center.y();
p.x() = grid_center.x() + dy;
p.y() = grid_center.y() + dx;
}
}
}
@@ -3341,6 +3356,14 @@ bool FillRectilinear::fill_surface_trapezoidal(
break;
}
// Orca: cases 1 & 2 build the pattern symmetrically around the origin, so on their own they
// phase to the global origin and every part shares one grid. Shift the pattern onto the box
// center this->bounding_box carries, so separated infills align each part on itself. The center
// is the origin for a standalone object (or when the feature is off), making this a no-op there.
if (Pattern_type != 0)
for (Polyline &pl : polylines)
pl.translate(rotate_vector.second);
// Apply multiline fill
multiline_fill(polylines, params, spacing);
+11 -11
View File
@@ -230,8 +230,8 @@ static void getNamedSolids(const TopLoc_Location& location,
}
//bool load_step(const char *path, Model *model, bool& is_cancel,
// double linear_defletion/*=0.003*/,
// double angle_defletion/*= 0.5*/,
// double linear_deflection/*=0.003*/,
// double angle_deflection/*= 0.5*/,
// bool isSplitCompound,
// ImportStepProgressFn stepFn, StepIsUtf8Fn isUtf8Fn, long& mesh_face_num)
//{
@@ -288,7 +288,7 @@ static void getNamedSolids(const TopLoc_Location& location,
// stl.resize(namedSolids.size());
// tbb::parallel_for(tbb::blocked_range<size_t>(0, namedSolids.size()), [&](const tbb::blocked_range<size_t> &range) {
// for (size_t i = range.begin(); i < range.end(); i++) {
// BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_defletion, false, angle_defletion, true);
// BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_deflection, false, angle_deflection, true);
// // BBS: calculate total number of the nodes and triangles
// int aNbNodes = 0;
// int aNbTriangles = 0;
@@ -511,8 +511,8 @@ Step::Step_Status Step::load()
Step::Step_Status Step::mesh(Model* model,
bool& is_cancel,
bool isSplitCompound,
double linear_defletion/*=0.003*/,
double angle_defletion/*= 0.5*/)
double linear_deflection/*=0.003*/,
double angle_deflection/*= 0.5*/)
{
bool task_result = false;
@@ -544,7 +544,7 @@ Step::Step_Status Step::mesh(Model* model,
stl.resize(namedSolids.size());
tbb::parallel_for(tbb::blocked_range<size_t>(0, namedSolids.size()), [&](const tbb::blocked_range<size_t>& range) {
for (size_t i = range.begin(); i < range.end(); i++) {
BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_defletion, false, angle_defletion, true);
BRepMesh_IncrementalMesh mesh(namedSolids[i].solid, linear_deflection, false, angle_deflection, true);
// BBS: calculate total number of the nodes and triangles
int aNbNodes = 0;
int aNbTriangles = 0;
@@ -689,15 +689,15 @@ void Step::clean_mesh_data()
}
}
unsigned int Step::get_triangle_num(double linear_defletion, double angle_defletion)
unsigned int Step::get_triangle_num(double linear_deflection, double angle_deflection)
{
unsigned int tri_num = 0;
try {
Handle(StepProgressIncdicator) progress = new StepProgressIncdicator(m_stop_mesh);
clean_mesh_data();
IMeshTools_Parameters param;
param.Deflection = linear_defletion;
param.Angle = angle_defletion;
param.Deflection = linear_deflection;
param.Angle = angle_deflection;
param.InParallel = true;
for (int i = 0; i < m_name_solids.size(); ++i) {
BRepMesh_IncrementalMesh mesh(m_name_solids[i].solid, param, progress->Start());
@@ -719,7 +719,7 @@ unsigned int Step::get_triangle_num(double linear_defletion, double angle_deflet
return tri_num;
}
unsigned int Step::get_triangle_num_tbb(double linear_defletion, double angle_defletion)
unsigned int Step::get_triangle_num_tbb(double linear_deflection, double angle_deflection)
{
unsigned int tri_num = 0;
clean_mesh_data();
@@ -727,7 +727,7 @@ unsigned int Step::get_triangle_num_tbb(double linear_defletion, double angle_de
[&](const tbb::blocked_range<size_t>& range) {
for (size_t i = range.begin(); i < range.end(); i++) {
unsigned int solids_tri_num = 0;
BRepMesh_IncrementalMesh mesh(m_name_solids[i].solid, linear_defletion, false, angle_defletion, true);
BRepMesh_IncrementalMesh mesh(m_name_solids[i].solid, linear_deflection, false, angle_deflection, true);
for (TopExp_Explorer anExpSF(m_name_solids[i].solid, TopAbs_FACE); anExpSF.More(); anExpSF.Next()) {
TopLoc_Location aLoc;
Handle(Poly_Triangulation) aTriangulation = BRep_Tool::Triangulation(TopoDS::Face(anExpSF.Current()), aLoc);
+6 -6
View File
@@ -38,8 +38,8 @@ struct NamedSolid
//BBS: Load an step file into a provided model.
extern bool load_step(const char *path, Model *model,
bool& is_cancel,
double linear_defletion = 0.003,
double angle_defletion = 0.5,
double linear_deflection = 0.003,
double angle_deflection = 0.5,
bool isSplitCompound = false,
ImportStepProgressFn proFn = nullptr,
StepIsUtf8Fn isUtf8Fn = nullptr,
@@ -98,14 +98,14 @@ public:
Step(std::string path, ImportStepProgressFn stepFn = nullptr, StepIsUtf8Fn isUtf8Fn = nullptr);
~Step();
Step_Status load();
unsigned int get_triangle_num(double linear_defletion, double angle_defletion);
unsigned int get_triangle_num_tbb(double linear_defletion, double angle_defletion);
unsigned int get_triangle_num(double linear_deflection, double angle_deflection);
unsigned int get_triangle_num_tbb(double linear_deflection, double angle_deflection);
void clean_mesh_data();
Step_Status mesh(Model* model,
bool& is_cancel,
bool isSplitCompound,
double linear_defletion = 0.003,
double angle_defletion = 0.5);
double linear_deflection = 0.003,
double angle_deflection = 0.5);
std::atomic<bool> m_stop_mesh{false};
void update_process(int load_stage, int current, int total, bool& cancel);
+168 -11
View File
@@ -347,6 +347,7 @@ static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_laye
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
static constexpr const char* FILAMENT_VOL_MAP_ATTR = "filament_volume_maps";
static constexpr const char* LIMIT_FILAMENT_MAP_ATTR = "limit_filament_maps";
static constexpr const char* GCODE_FILE_ATTR = "gcode_file";
static constexpr const char* THUMBNAIL_FILE_ATTR = "thumbnail_file";
@@ -699,6 +700,35 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
info.id = it->first;
info.used_g = used_filament_g;
info.used_m = used_filament_m;
// Stamp each filament's logical-nozzle assignment onto the saved 3mf so the device/monitor can
// reconstruct it. This block runs for every print: reorder_extruders_for_minimum_flush_volume
// runs unconditionally and stores a (non-null) 1-nozzle result even for a single-extruder print,
// so result->nozzle_group_result is non-null here for single-nozzle printers too. The stamped
// nozzle_diameter is the grouping result's rounded matching-key value; the 3mf writer decides the
// final saved diameter (see has_multi_nozzle_extruder). group_id and volume_type are unaffected.
if (result && result->nozzle_group_result) {
auto nozzles_for_filament = result->nozzle_group_result->get_nozzles_for_filament(it->first);
if (!nozzles_for_filament.empty()) {
info.group_id.reserve(nozzles_for_filament.size());
std::set<double> diameters;
std::set<NozzleVolumeType> volume_types;
for (const auto& nozzle : nozzles_for_filament) {
info.group_id.emplace_back(nozzle.group_id);
diameters.insert(string_to_double_decimal_point(nozzle.diameter));
volume_types.insert(nozzle.volume_type);
}
std::sort(info.group_id.begin(), info.group_id.end());
info.group_id.erase(std::unique(info.group_id.begin(), info.group_id.end()), info.group_id.end());
if (!diameters.empty())
info.nozzle_diameter = *diameters.begin();
if (volume_types.size() > 1)
info.nozzle_volume_type = get_nozzle_volume_type_string(nvtHybrid);
else if (!volume_types.empty())
info.nozzle_volume_type = get_nozzle_volume_type_string(*volume_types.begin());
}
}
auto model_volume_it = ps.model_volumes_per_extruder.find(it->first);
auto support_volume_it = ps.support_volumes_per_extruder.find(it->first);
info.used_for_object = model_volume_it != ps.model_volumes_per_extruder.end() && model_volume_it->second > EPSILON;
@@ -706,6 +736,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
slice_filaments_info.push_back(info);
}
// Carry the layer-aware grouping result into the plate so the 3mf writer can emit the <nozzle> tags
// and the enable_filament_dynamic_map flag. Only a LayeredNozzleGroupResult (the slicer output) is
// stored; a device-side StaticNozzleGroupResult loaded from a 3mf is not re-serialized here.
auto layered_group_result = std::dynamic_pointer_cast<MultiNozzleUtils::LayeredNozzleGroupResult>(result->nozzle_group_result);
if (layered_group_result)
nozzle_group_result = *layered_group_result;
/* only for test
GCodeProcessorResult::SliceWarning sw;
sw.msg = BED_TEMP_TOO_HIGH_THAN_FILAMENT;
@@ -1283,6 +1320,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_start_config_warning(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_warning();
bool _handle_start_config_nozzle(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_nozzle();
//BBS: add plater config parse functions
bool _handle_start_config_plater(const char** attributes, unsigned int num_attributes);
bool _handle_end_config_plater();
@@ -1618,8 +1658,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
plate->is_label_object_enabled = it->second->is_label_object_enabled;
plate->skipped_objects = it->second->skipped_objects;
plate->slice_filaments_info = it->second->slice_filaments_info;
plate->nozzles_info = it->second->nozzles_info;
plate->printer_model_id = it->second->printer_model_id;
plate->nozzle_diameters = it->second->nozzle_diameters;
plate->nozzle_volume_types = it->second->nozzle_volume_types;
plate->filament_maps = it->second->filament_maps;
plate->filament_change_sequence = it->second->filament_change_sequence;
plate->nozzle_change_sequence = it->second->nozzle_change_sequence;
@@ -2289,9 +2331,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
plate_data_list[it->first-1]->is_support_used = it->second->is_support_used;
plate_data_list[it->first-1]->is_label_object_enabled = it->second->is_label_object_enabled;
plate_data_list[it->first-1]->slice_filaments_info = it->second->slice_filaments_info;
plate_data_list[it->first-1]->nozzles_info = it->second->nozzles_info;
plate_data_list[it->first-1]->skipped_objects = it->second->skipped_objects;
plate_data_list[it->first-1]->printer_model_id = it->second->printer_model_id;
plate_data_list[it->first-1]->nozzle_diameters = it->second->nozzle_diameters;
plate_data_list[it->first-1]->nozzle_volume_types = it->second->nozzle_volume_types;
plate_data_list[it->first-1]->filament_maps = it->second->filament_maps;
plate_data_list[it->first-1]->filament_change_sequence = it->second->filament_change_sequence;
plate_data_list[it->first-1]->nozzle_change_sequence = it->second->nozzle_change_sequence;
@@ -3469,6 +3513,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
res = _handle_start_config_filament(attributes, num_attributes);
else if (::strcmp(SLICE_WARNING_TAG, name) == 0)
res = _handle_start_config_warning(attributes, num_attributes);
else if (::strcmp(NOZZLE_TAG, name) == 0)
res = _handle_start_config_nozzle(attributes, num_attributes);
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
res = _handle_start_assemble(attributes, num_attributes);
else if (::strcmp(ASSEMBLE_ITEM_TAG, name) == 0)
@@ -3503,6 +3549,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
res = _handle_end_config_plater();
else if (::strcmp(FILAMENT_TAG, name) == 0)
res = _handle_end_config_filament();
else if (::strcmp(NOZZLE_TAG, name) == 0)
res = _handle_end_config_nozzle();
else if (::strcmp(INSTANCE_TAG, name) == 0)
res = _handle_end_config_plater_instance();
else if (::strcmp(ASSEMBLE_TAG, name) == 0)
@@ -4460,6 +4508,21 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_plater->config.set_key_value("filament_map", new ConfigOptionInts(filament_map));
}
}
else if (key == FILAMENT_VOL_MAP_ATTR) {
if (m_curr_plater){
auto filament_volume_map = get_vector_from_string(value);
for (size_t idx = 0; idx < filament_volume_map.size(); ++idx) {
// The map feeds per-filament slot resolution and grouping. Clamp any
// higher volume-type back to Standard(0) on load: Hybrid(2) is only an
// in-memory grouping seed that is never persisted, and TPU High Flow(3)
// is clamped with the same information loss on every load.
if (filament_volume_map[idx] > 1) {
filament_volume_map[idx] = 0;
}
}
m_curr_plater->config.set_key_value("filament_volume_map", new ConfigOptionInts(filament_volume_map));
}
}
else if (key == GCODE_FILE_ATTR)
{
m_curr_plater->gcode_file = value;
@@ -4569,6 +4632,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (m_curr_plater)
m_curr_plater->printer_model_id = value;
}
else if (key == NOZZLE_VOLUME_TYPE_ATTR)
{
if (m_curr_plater)
m_curr_plater->nozzle_volume_types = value;
}
else if (key == NOZZLE_DIAMETERS_ATTR)
{
if (m_curr_plater)
@@ -4622,6 +4690,41 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Importer::_handle_start_config_nozzle(const char** attributes, unsigned int num_attributes)
{
// Read the per-plate <nozzle> tags. Older 3mf without <nozzle> tags leave nozzles_info
// empty; load_nozzle_infos_with_compatibility then rebuilds the list from the per-filament
// group_id / filament_map on the device side.
if (m_curr_plater) {
// id="0" extruder_id="1" nozzle_diameter="0.4" volume_type="Standard"
std::string id = bbs_get_attribute_value_string(attributes, num_attributes, "id");
std::string extruder_id = bbs_get_attribute_value_string(attributes, num_attributes, "extruder_id");
std::string nozzle_diameter= bbs_get_attribute_value_string(attributes, num_attributes, "nozzle_diameter");
std::string volume_type = bbs_get_attribute_value_string(attributes, num_attributes, "volume_type");
auto volume_type_str_to_enum = ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
MultiNozzleUtils::NozzleInfo nozzle_info;
nozzle_info.group_id = atoi(id.c_str());
nozzle_info.extruder_id = atoi(extruder_id.c_str()) - 1;
nozzle_info.diameter = nozzle_diameter;
if (volume_type_str_to_enum.count(volume_type))
nozzle_info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(volume_type));
else
nozzle_info.volume_type = NozzleVolumeType::nvtStandard;
m_curr_plater->nozzles_info.push_back(nozzle_info);
}
return true;
}
bool _BBS_3MF_Importer::_handle_end_config_nozzle()
{
// do nothing
return true;
}
bool _BBS_3MF_Importer::_handle_start_config_warning(const char** attributes, unsigned int num_attributes)
{
if (m_curr_plater) {
@@ -5949,7 +6052,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_thumbnail_middle = iter->second;
}
boost::system::error_code ec;
std::string filename = std::string(store_params.path);
std::string filename = store_params.path;
boost::filesystem::remove(filename + ".tmp", ec);
bool result = _save_model_to_file(filename + ".tmp", *store_params.model, store_params.plate_data_list, store_params.project_presets, store_params.config,
@@ -7980,6 +8083,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << "\"/>\n";
}
ConfigOptionInts* filament_volume_maps_opt = plate_data->config.option<ConfigOptionInts>("filament_volume_map");
if (filament_map_mode_opt != nullptr && filament_volume_maps_opt != nullptr) {
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << FILAMENT_VOL_MAP_ATTR << "\" " << VALUE_ATTR << "=\"";
const std::vector<int>& volume_values = filament_volume_maps_opt->values;
for (int i = 0; i < volume_values.size(); ++i) {
stream << volume_values[i];
if (i != (volume_values.size() - 1))
stream << " ";
}
stream << "\"/>\n";
}
if (save_gcode)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << GCODE_FILE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << xml_escape(plate_data->gcode_file) << "\"/>\n";
if (!plate_data->gcode_file.empty()) {
@@ -8119,7 +8234,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
[](unsigned int filament_id) { return filament_id + 1; });
const std::string plate_key = "plate_" + std::to_string(idx + 1);
sequence_json[plate_key]["sequence"] = filament_sequence;
// Dynamic-map plates write the sequence under "filament_sequence"; every other plate (the
// whole shipping fleet + H2C static mode) keeps the "sequence" key, so the saved 3mf is
// byte-identical to the older format. The reader accepts both.
const bool enable_dynamic_map = plate_data->nozzle_group_result && plate_data->nozzle_group_result->is_support_dynamic_nozzle_map();
const std::string seq_key = enable_dynamic_map ? "filament_sequence" : "sequence";
sequence_json[plate_key][seq_key] = filament_sequence;
sequence_json[plate_key]["nozzle_sequence"] = plate_data->nozzle_change_sequence;
sequence_json[plate_key]["optimal_assignment"] = plate_data->optimal_assignment;
}
@@ -8205,6 +8325,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (nozzle_diameter_option)
nozzle_diameters_str = nozzle_diameter_option->serialize();
// True when any extruder carries a cluster of interchangeable nozzles (max nozzle count
// > 1). Such an extruder's per-nozzle diameters are not expressible in the per-extruder
// nozzle_diameter config, so the saved <filament>/<nozzle> diameters must come from the
// grouping result. For a single-nozzle-per-extruder printer the true diameter is the raw
// config value; the grouping result rounds it to the nearest of {0.2,0.4,0.6,0.8} for its
// internal matching key, so reading that back would rewrite a non-standard nozzle
// (e.g. 0.5 -> 0.4). Use this flag to keep the exact config value in that case.
auto* extruder_max_nozzle_count_option = dynamic_cast<const ConfigOptionInts*>(config.option("extruder_max_nozzle_count"));
const bool has_multi_nozzle_extruder = extruder_max_nozzle_count_option &&
std::any_of(extruder_max_nozzle_count_option->values.begin(), extruder_max_nozzle_count_option->values.end(),
[](int v) { return v > 1; });
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << PRINTER_MODEL_ID_ATTR << "\" " << VALUE_ATTR << "=\"" << plate_data->printer_model_id << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << NOZZLE_DIAMETERS_ATTR << "\" " << VALUE_ATTR << "=\"" << nozzle_diameters_str << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TIMELAPSE_TYPE_ATTR << "\" " << VALUE_ATTR << "=\"" << timelapse_type << "\"/>\n";
@@ -8214,7 +8346,15 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << OUTSIDE_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->toolpath_outside << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SUPPORT_USED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_support_used << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << LABEL_OBJECT_ENABLED_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha<< plate_data->is_label_object_enabled << "\"/>\n";
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n";
// Report the plate's dynamic-map state from the grouping result. The result is present
// for the whole fleet (a static single-nozzle result too), so this if-branch is normally
// taken; is_support_dynamic_nozzle_map() is false for any non-dynamic (static /
// single-extruder) result ⇒ byte-identical to the previously hard-coded value. The else
// is a defensive fallback for a missing result.
if (plate_data && plate_data->nozzle_group_result)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << plate_data->nozzle_group_result->is_support_dynamic_nozzle_map() << "\"/>\n";
else
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << ENABLE_FILAMENT_DYNAMIC_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << false << "\"/>\n";
{
bool has_filament_switcher = config.has("has_filament_switcher") ? config.opt_bool("has_filament_switcher") : false;
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << HAS_FILAMENT_SWITCHER_ATTR << "\" " << VALUE_ATTR << "=\"" << std::boolalpha << has_filament_switcher << "\"/>\n";
@@ -8329,7 +8469,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (std::find(used_nozzle_groups.begin(), used_nozzle_groups.end(), nozzle_group_id) == used_nozzle_groups.end())
used_nozzle_groups.push_back(nozzle_group_id);
const std::string filament_nozzle_group_id = it->group_id.empty() ? std::to_string(nozzle_group_id) : join_int_list_comma(it->group_id);
const double filament_nozzle_diameter = it->nozzle_diameter > 0.0 ? it->nozzle_diameter : get_nozzle_diameter(nozzle_group_id);
// Single-nozzle extruders: exact config diameter; clusters keep the result's rounded
// value (see has_multi_nozzle_extruder).
const double filament_nozzle_diameter = (has_multi_nozzle_extruder && it->nozzle_diameter > 0.0)
? it->nozzle_diameter : get_nozzle_diameter(nozzle_group_id);
const std::string filament_nozzle_volume_type = it->nozzle_volume_type.empty() ? get_nozzle_volume_type(nozzle_group_id) : it->nozzle_volume_type;
stream << " <" << FILAMENT_TAG << " " << FILAMENT_ID_TAG << "=\"" << std::to_string(it->id + 1) << "\" "
@@ -8349,12 +8492,26 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <" << SLICE_WARNING_TAG << " msg=\"" << it->msg << "\" level=\"" << std::to_string(it->level) << "\" error_code =\"" << it->error_code << "\" />\n";
}
for (int nozzle_group_id : used_nozzle_groups) {
stream << " <" << NOZZLE_TAG << " "
<< "id=\"" << nozzle_group_id << "\" "
<< "extruder_id=\"" << nozzle_group_id + 1 << "\" "
<< "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" "
<< "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n";
// Emit the <nozzle> tags from the grouping result. Single-nozzle-per-extruder printers
// override the diameter with the exact config value (see has_multi_nozzle_extruder); the
// else is a defensive fallback for a missing result.
if (plate_data->nozzle_group_result) {
auto used_nozzle_list = plate_data->nozzle_group_result->get_used_nozzles_in_extruder();
for (auto& used_nozzle : used_nozzle_list) {
if (!has_multi_nozzle_extruder && nozzle_diameter_option &&
used_nozzle.extruder_id >= 0 && used_nozzle.extruder_id < (int) nozzle_diameter_option->values.size()) {
used_nozzle.diameter = get_nozzle_diameter_str(used_nozzle.extruder_id);
}
stream << " <" << NOZZLE_TAG << " " << used_nozzle.serialize() << "/>\n";
}
} else {
for (int nozzle_group_id : used_nozzle_groups) {
stream << " <" << NOZZLE_TAG << " "
<< "id=\"" << nozzle_group_id << "\" "
<< "extruder_id=\"" << nozzle_group_id + 1 << "\" "
<< "nozzle_diameter=\"" << get_nozzle_diameter_str(nozzle_group_id) << "\" "
<< "volume_type=\"" << get_nozzle_volume_type(nozzle_group_id) << "\"/>\n";
}
}
if (!plate_data->layer_filaments.empty()) {
@@ -8988,7 +9145,7 @@ bool store_bbs_3mf(StoreParams& store_params)
// All export should use "C" locales for number formatting.
CNumericLocalesSetter locales_setter;
if (store_params.path == nullptr || store_params.model == nullptr)
if (store_params.path.empty() || store_params.model == nullptr)
return false;
_BBS_3MF_Exporter exporter;
+9 -1
View File
@@ -73,6 +73,7 @@ struct PlateData
std::map<int, std::pair<int, int>> obj_inst_map;
std::string printer_model_id;
std::string nozzle_diameters;
std::string nozzle_volume_types;
std::string gcode_file;
std::string gcode_file_md5;
std::string thumbnail_file;
@@ -102,6 +103,13 @@ struct PlateData
std::vector<unsigned int> nozzle_change_sequence;
std::vector<int> optimal_assignment;
// Multi-nozzle grouping surface. nozzles_info accumulates the <nozzle> tags read from a
// gcode.3mf; nozzle_group_result is the slicer's per-filament→nozzle assignment carried into the
// saved 3mf metadata (write) and reconstructed on load. Both are empty/nullopt for single-nozzle
// prints, so the saved-3mf output for single-nozzle printers is byte-identical.
std::vector<MultiNozzleUtils::NozzleInfo> nozzles_info;
std::optional<MultiNozzleUtils::LayeredNozzleGroupResult> nozzle_group_result;
// Hexadecimal number,
// the 0th digit corresponds to extruder 1
// the 1th digit corresponds to extruder 2
@@ -226,7 +234,7 @@ typedef std::map<int, PlateData*> PlateDataMaps;
struct StoreParams
{
const char* path;
std::string path;
Model* model = nullptr;
PlateDataPtrs plate_data_list;
int export_plate_idx = -1;
+1272 -280
View File
File diff suppressed because it is too large Load Diff
+68 -3
View File
@@ -60,15 +60,20 @@ class Wipe {
public:
bool enable;
Polyline path;
// Orca:
struct RetractionValues{
double retractLengthBeforeWipe;
double retractLengthDuringWipe;
double retraction_length_before_wipe = 0.;
double retraction_length_during_wipe = 0.;
double retraction_length_after_wipe = 0.;
};
Wipe() : enable(false) {}
bool has_path() const { return !this->path.points.empty(); }
void reset_path() { this->path = Polyline(); }
std::string wipe(GCode &gcodegen, double length, bool toolchange = false, bool is_last = false);
// Orca:
RetractionValues calculateWipeRetractionLengths(GCode& gcodegen, bool toolchange);
};
@@ -252,7 +257,8 @@ public:
std::string travel_to(const Point& point, ExtrusionRole role, std::string comment, double z = DBL_MAX);
bool needs_retraction(const Polyline& travel, ExtrusionRole role, LiftType& lift_type);
std::string retract(bool toolchange = false, bool is_last_retraction = false, LiftType lift_type = LiftType::NormalLift, bool apply_instantly = false, ExtrusionRole role = erNone);
std::string unretract() { return m_writer.unlift() + m_writer.unretract(); }
// 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);
bool is_BBL_Printer();
WipeTowerType wipe_tower_type();
@@ -263,6 +269,12 @@ public:
// append full config to the given string
static void append_full_config(const Print& print, std::string& str);
// Per-filament config-slot resolvers for the current layer (m_cur_layer_idx): the filament
// resolver keys filament-indexed arrays, the nozzle resolver keys (extruder x volume-type)
// slot arrays. Both degenerate to filament_id / extruder index on single-volume printers.
size_t get_filament_config_index(int filament_id) const;
size_t get_nozzle_config_index(int filament_id) const;
// Object and support extrusions of the same PrintObject at the same print_z.
// public, so that it could be accessed by free helper functions from GCode.cpp
struct LayerToPrint
@@ -394,6 +406,7 @@ private:
void check_placeholder_parser_failed();
size_t cur_extruder_index() const;
size_t get_extruder_id(unsigned int filament_id) const;
void update_placeholder_parser_with_variant_params();
void set_last_pos(const Point &pos) { m_last_pos = Point3(pos, 0); m_last_pos_defined = true; }
void set_last_pos(const Point3 &pos) { m_last_pos = pos; m_last_pos_defined = true; }
@@ -402,6 +415,11 @@ private:
std::string preamble();
// BBS
std::string change_layer(coordf_t print_z);
// Bedslinger model: derive the Y-axis acceleration limit from the machine force/bed-mass config
// and the mass already printed. Yields the min machine Y acceleration when the A2L config keys are
// unset (i.e. every existing printer), so it is inert for them.
void mass_load_limited_machine_acceleration(const PrintStatistics &curr_print_statistics, const Print &print,
double &y_acceleration_limit_res, double &accumulated_mass_res);
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
// should be executed
std::string extrude_entity(const ExtrusionEntity& entity,
@@ -500,6 +518,18 @@ private:
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
// Farthest-point timelapse: find the extrusion point farthest from camera (0,0)
void compute_farthest_point(const std::vector<LayerToPrint> &layers, int most_used_extruder,
const std::map<std::pair<const SupportLayer *, ExtrusionRole>, unsigned int> &support_filaments);
// Build the per-layer timelapse snapshot g-code (safe-position or, when skip_pos_pick,
// an inline photo at the current head position). Extracted from the former process_layer lambda so the
// per-extrusion farthest-point hook (_extrude) can call it too. Identical to the old lambda when the
// farthest-point subsystem is disabled (skip_pos_pick=false, m_farthest_point_timelapse.enabled=false).
std::string generate_timelapse_gcode(const Print &print, coordf_t print_z, int most_used_extruder,
const std::set<size_t> *layer_object_label_ids,
const std::vector<const PrintObject*> *printed_objects,
bool skip_pos_pick = false);
// BBS
LiftType to_lift_type(ZHopType z_hop_types);
@@ -556,6 +586,32 @@ private:
AvoidCrossingPerimeters m_avoid_crossing_perimeters;
RetractWhenCrossingPerimeters m_retract_when_crossing_perimeters;
TimelapsePosPicker m_timelapse_pos_picker;
// Farthest-point timelapse context. Corexy-only refinement layered on top of the existing
// timelapse_type. All fields default to the inert state; `enabled` is (re)computed each layer in
// process_layer and is false whenever the farthest_point_timelapse config toggle is off, the printer
// is i3 (psI3), or timelapse_type is not traditional — so every shipping printer that does not set the
// toggle is identical to the previous path.
struct FarthestPointTimelapseContext {
// Whether farthest-point timelapse is active for this layer
bool enabled{false};
// The farthest extrusion point from camera (0,0) in global scaled coordinates (includes plate origin + inst.shift)
Point farthest_point;
// farthest_point converted to mm (gcode coordinate space, includes plate origin)
Vec2d farthest_gcode_pos{0, 0};
// Extruder index (0-based) that prints the farthest point
int farthest_extruder_id{0};
// Whether the farthest point is printed by the photo head (most_used_extruder)
bool farthest_is_photo_head{false};
// Whether inline timelapse gcode has already been inserted on this layer
bool inserted_this_layer{false};
// The extruder used most on this layer, chosen as the photo head
int most_used_extruder{0};
// Object labels for the current layer, used when inline timelapse is inserted from extrusion code.
std::set<size_t> layer_object_label_ids;
};
FarthestPointTimelapseContext m_farthest_point_timelapse;
bool m_enable_loop_clipping;
//resonance avoidance
bool m_resonance_avoidance;
@@ -595,6 +651,9 @@ private:
float m_last_layer_z{ 0.0f };
float m_max_layer_z{ 0.0f };
float m_last_width{ 0.0f };
// Bedslinger mass model: cumulative printed mass at the previous layer, used to derive
// the current layer mass for the per-layer Y acceleration limit (curr_y_acceleration_limit).
double m_last_layer_accumulated_mass{ 0.0 };
// Always check gcode placeholders when building in debug mode.
#if !defined(NDEBUG)
@@ -654,12 +713,18 @@ private:
int m_start_gcode_filament = -1;
std::string m_filament_instances_code;
// Object layer id of the layer being generated; keys the per-filament config-slot
// resolvers. Distinct from m_layer_index (an export progress counter starting at -1).
size_t m_cur_layer_idx{0};
std::set<unsigned int> m_initial_layer_extruders;
std::vector<std::vector<unsigned int>> m_sorted_layer_filaments;
// BBS
int get_bed_temperature(const int extruder_id, const bool is_first_layer, const BedType bed_type) const;
int get_highest_bed_temperature(const bool is_first_layer,const Print &print) const;
void update_layer_related_config(int layer_id);
double calc_max_volumetric_speed(const double layer_height, const double line_width, const std::string co_str);
std::string _extrude(const ExtrusionPath &path, std::string description = "", double speed = -1);
bool _needSAFC(const ExtrusionPath &path);
@@ -0,0 +1,190 @@
#include "GCodeProcessor.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <string_view>
namespace Slic3r {
namespace {
bool equals_case_insensitive(std::string_view lhs, std::string_view rhs)
{
return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin(), [](unsigned char l, unsigned char r) {
return std::tolower(l) == std::tolower(r);
});
}
float get_clamped_param(const GCodeReader::GCodeLine& line, char axis, float default_value, float min_value, float max_value)
{
float value = default_value;
line.has_value(axis, value);
return std::clamp(value, min_value, max_value);
}
float extrusion_time(float e_length, float feedrate)
{
return feedrate > 0.0f && e_length > 0.0f ? e_length / feedrate * 60.0f : 0.0f;
}
float retract_time(float e_length)
{
static constexpr float retract_feedrate = 1800.0f;
return extrusion_time(std::max(e_length, 0.0f), retract_feedrate);
}
float s819_time(float e_length, float feedrate)
{
static constexpr float s819_tail_flush_length = 10.0f;
static constexpr float s819_tail_feedrate = 400.0f;
const float tail_length = std::min(std::max(e_length, 0.0f), s819_tail_flush_length);
const float main_length = std::max(e_length - tail_length, 0.0f);
return extrusion_time(main_length, feedrate) + extrusion_time(tail_length, s819_tail_feedrate);
}
float estimate_M6211_time_for_centauri_carbon(const GCodeReader::GCodeLine& line, float length, double current_x,
double current_y)
{
static constexpr float max_segment_length = 73.0f;
static constexpr float wipe_after_flush_time = 2.8f;
static constexpr float main_feedrate = 500.0f;
static constexpr float tail_feedrate = 400.0f;
static constexpr float travel_feedrate = 5000.0f;
static constexpr double parking_x = 256.0;
static constexpr double parking_y = 0.0;
const float flush_length = std::clamp(length, 10.0f, 1000.0f);
const float cool_time = get_clamped_param(line, 'P', 5000.0f, 0.0f, 20000.0f) * 0.001f;
const float travel_time = static_cast<float>(std::abs(current_y - parking_y) + std::abs(current_x - parking_x)) /
travel_feedrate * 60.0f;
// Initial time, including: material change, heating, etc.
float m6211_time = 18.2f + travel_time;
float remaining_flush_length = std::max(flush_length, 0.0f);
while (remaining_flush_length > 0.0f) {
const float segment_length = std::min(remaining_flush_length, max_segment_length);
remaining_flush_length -= segment_length;
if (segment_length >= max_segment_length) {
// Full segment: 3-phase extrusion (30+35+10=75mm) + retract
m6211_time += extrusion_time(30.0f, main_feedrate) + extrusion_time(35.0f, main_feedrate) +
extrusion_time(10.0f, tail_feedrate) + extrusion_time(2.0f, tail_feedrate) + cool_time +
wipe_after_flush_time;
} else {
// Partial last segment: simple extrude at F500 + retract at F400
m6211_time += extrusion_time(segment_length, main_feedrate) + extrusion_time(2.0f, tail_feedrate) + cool_time +
wipe_after_flush_time;
}
}
return m6211_time;
}
float estimate_M6211_time_for_centauri_carbon_2(const GCodeReader::GCodeLine& line, float length, float new_extruder_temp)
{
const float flush_length = std::clamp(length, 10.0f, 1000.0f);
const float flush_length_single = get_clamped_param(line, 'K', 75.0f, 10.0f, 300.0f);
const float old_filament_e_feedrate = get_clamped_param(line, 'M', 300.0f, 10.0f, 600.0f);
const float new_filament_e_feedrate = get_clamped_param(line, 'N', 300.0f, 10.0f, 600.0f);
const float cool_time = get_clamped_param(line, 'P', 3000.0f, 0.0f, 20000.0f) * 0.001f;
// The flush length of the old material, unit: mm
static constexpr float e_flush_dist = 15.0f;
// Wipe time after flush, in seconds
static constexpr float wipe_after_flush_time = 5.0f;
const float flush_length_after_start = std::max(flush_length - e_flush_dist, 0.0f);
const int flush_times = std::max(1, static_cast<int>(std::ceil(flush_length_after_start / flush_length_single)));
const float flush_length_actual = flush_length_single;
// Initial time, including: material change, heating, moving, etc.
float m6211_time = 31.0f;
m6211_time += extrusion_time(std::min(e_flush_dist, flush_length), old_filament_e_feedrate);
const int intermediate_flush_times = flush_times - 1;
const float intermediate_flush_time = s819_time(flush_length_actual, new_filament_e_feedrate) + retract_time(6.0f) + cool_time +
wipe_after_flush_time;
m6211_time += static_cast<float>(intermediate_flush_times) * intermediate_flush_time;
m6211_time += s819_time(flush_length_actual, new_filament_e_feedrate * 0.8f) + retract_time(4.0f) + cool_time + wipe_after_flush_time;
static constexpr float cooling_rate = 1.36f;
const float r_temp = get_clamped_param(line, 'R', new_extruder_temp + 20.0f, 185.0f, 350.0f);
const float s_temp = get_clamped_param(line, 'S', 250.0f, 185.0f, 350.0f);
if (s_temp < r_temp)
m6211_time += (r_temp - s_temp) / cooling_rate;
return m6211_time;
}
float estimate_M6211_time(const GCodeReader::GCodeLine& line, std::string_view printer_model, float length, float new_extruder_temp, double current_x, double current_y)
{
if (equals_case_insensitive(printer_model, "Elegoo Centauri Carbon") || equals_case_insensitive(printer_model, "Elegoo Centauri")) {
return estimate_M6211_time_for_centauri_carbon(line, length, current_x, current_y);
} else if (equals_case_insensitive(printer_model, "Elegoo Centauri Carbon 2") ||
equals_case_insensitive(printer_model, "Elegoo Centauri 2")) {
return estimate_M6211_time_for_centauri_carbon_2(line, length, new_extruder_temp);
}
return 0.0f;
}
} // namespace
void GCodeProcessor::process_elegoo_M6211(const GCodeReader::GCodeLine& line)
{
float length = 0.0f;
if (!line.has_value('L', length) || length <= 0.0f)
return;
float t = -1.0f;
if (!line.has_value('T', t) || t < 0.0f)
return;
const int filament_id = static_cast<int>(std::round(t));
if (filament_id < 0 || filament_id >= m_result.filaments_count)
return;
const int extruder_id = m_filament_maps[filament_id];
float new_extruder_temp = 0.0f;
if (line.has_value('S', new_extruder_temp)) {
if (extruder_id >= 0 && static_cast<size_t>(extruder_id) < m_extruder_temps.size())
m_extruder_temps[static_cast<size_t>(extruder_id)] = new_extruder_temp;
}
const float m6211_time = estimate_M6211_time(line, m_printer_model, length, new_extruder_temp,
m_start_position[X], m_start_position[Y]);
const int curr_filament_id = get_filament_id(false);
const bool is_first_extrusion = (curr_filament_id == -1) || (filament_id == curr_filament_id);
m_time_processor.filament_unload_times = 0;
m_time_processor.filament_load_times = m6211_time;
process_filament_change(filament_id);
if (extruder_id >= 0 && static_cast<size_t>(extruder_id) < m_remaining_volume.size()) {
const float remaining_volume = static_cast<size_t>(extruder_id) < m_nozzle_volume.size() ?
m_nozzle_volume[extruder_id] :
0.0f;
const float filament_diameter = static_cast<size_t>(filament_id) < m_result.filament_diameters.size() ?
m_result.filament_diameters[filament_id] :
m_result.filament_diameters.back();
const float area_filament_cross_section = static_cast<float>(M_PI) * sqr(0.5f * filament_diameter);
const float volume_flushed_filament = area_filament_cross_section * length;
if (volume_flushed_filament >= remaining_volume) {
if (!is_first_extrusion)
m_used_filaments.update_flush_per_filament(curr_filament_id, remaining_volume);
m_used_filaments.update_flush_per_filament(filament_id, volume_flushed_filament - remaining_volume);
m_remaining_volume[extruder_id] = 0.0f;
} else {
m_used_filaments.update_flush_per_filament(filament_id, volume_flushed_filament);
m_remaining_volume[extruder_id] -= volume_flushed_filament;
}
}
}
} // namespace Slic3r
File diff suppressed because it is too large Load Diff
+390 -6
View File
@@ -6,6 +6,7 @@
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include <cstdint>
#include <array>
@@ -43,6 +44,23 @@ class Print;
Count
};
// Classifies why a wipe-tower / change_filament / time-lapse region is safe to relocate a
// pre-heat M104 into, for the pre-heat/pre-cool injector. The shipping time_lapse_gcode
// template (timelapse-on by default) emits SKIPPABLE_* on essentially every slice, so the
// "timelapse" payload -> stTimelapse classification is exercised widely.
enum SkipType
{
stTimelapse,
stHeadWrapDetect,
stOther,
stNone
};
const std::unordered_map<std::string_view, SkipType> skip_type_map{
{"timelapse", SkipType::stTimelapse},
{"head_wrap_detect", SkipType::stHeadWrapDetect}
};
struct PrintEstimatedStatistics
{
enum class ETimeMode : unsigned char
@@ -77,6 +95,10 @@ class Print;
std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
unsigned int total_filament_changes;
// Number of filament changes that actually re-flush a nozzle (a filament-in-nozzle change
// onto a non-empty nozzle), tracked only by the richer multi-nozzle hotend-change time model.
// Stays 0 for single-nozzle printers (X1/P1/A1/H2S/A2L), which never enter the two-arg model.
unsigned int total_flush_filament_changes;
unsigned int total_extruder_changes;
float total_filament_load_time;
float total_filament_unload_time;
@@ -101,6 +123,7 @@ class Print;
flush_per_filament.clear();
used_filaments_per_role.clear();
total_filament_changes = 0;
total_flush_filament_changes = 0;
total_extruder_changes = 0;
total_filament_load_time = 0.0f;
total_filament_unload_time = 0.0f;
@@ -166,6 +189,14 @@ class Print;
ConflictResultOpt conflict_result;
GCodeCheckResult gcode_check_result;
FilamentPrintableResult filament_printable_reuslt;
// The per-filament -> logical-nozzle grouping the slicer computed for this
// result, surfaced onto the object the device GUI reads
// (plater->background_process().get_current_gcode_result()). Populated only from
// Print::get_layered_nozzle_group_result() (ToolOrdering's static L/R + rack subset);
// default-empty (null) and read by no g-code emitter, so it is invisible in the emitted
// g-code. Consumed by the print-dispatch nozzle mapping (DevNozzleMappingCtrl) via
// DevUtilBackend::GetNozzleGroupResult.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> nozzle_group_result;
float initial_layer_time;
struct SettingsIds
@@ -263,6 +294,14 @@ class Print;
std::vector<SliceWarning> warnings;
int nozzle_hrc;
std::vector<NozzleType> nozzle_type;
// Per-extruder physical hotend type. Fed to the pre-heat injector's TimeProcessContext
// (mixed-type X2D workaround). Populated in apply_config; unused until the injector side-pass
// consumes it.
std::vector<ExtruderType> extruder_types;
// Machine-slot layout of the per-variant printer arrays (one entry per (extruder x
// volume-type) slot). Populated in apply_config; keys the per-slot machine-limit lookup.
std::vector<std::string> printer_extruder_variant;
std::vector<int> printer_extruder_id;
// first key stores filaments, second keys stores the layer ranges(enclosed) that use the filaments
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
std::vector<unsigned int> nozzle_change_sequence;
@@ -271,6 +310,11 @@ class Print;
// first key stores `from` filament, second keys stores the `to` filament
std::map<std::pair<int,int>, int > filament_change_count_map;
// Accumulated print time spent inside SKIPPABLE regions, per skip type. Populated by the time
// estimator; consumed only downstream. The shipping time_lapse_gcode template emits SKIPPABLE_*
// widely, so this is typically populated (stTimelapse) on most slices.
std::unordered_map<SkipType, float> skippable_part_time;
BedType bed_type = BedType::btCount;
void reset();
@@ -304,11 +348,20 @@ class Print;
gcode_check_result = other.gcode_check_result;
limit_filament_maps = other.limit_filament_maps;
filament_printable_reuslt = other.filament_printable_reuslt;
// Orca: copy the shared grouping result so a copied result keeps it (shared_ptr =>
// memory-safe), rather than leaving a stale pointer on the target. No g-code effect either way.
nozzle_group_result = other.nozzle_group_result;
// Keep the per-extruder hotend types on a copied result (injector input).
extruder_types = other.extruder_types;
printer_extruder_variant = other.printer_extruder_variant;
printer_extruder_id = other.printer_extruder_id;
layer_filaments = other.layer_filaments;
filament_change_sequence = other.filament_change_sequence;
nozzle_change_sequence = other.nozzle_change_sequence;
optimal_assignment = other.optimal_assignment;
filament_change_count_map = other.filament_change_count_map;
// Keep the SKIPPABLE per-type time on a copied result.
skippable_part_time = other.skippable_part_time;
initial_layer_time = other.initial_layer_time;
#if ENABLE_GCODE_VIEWER_STATISTICS
time = other.time;
@@ -319,6 +372,75 @@ class Print;
void unlock() const { result_mutex.unlock(); }
};
// First-pass usage-block descriptors for the pre-heat/pre-cool injector. FilamentUsageBlock
// records the [lower,upper) output-line-id span a single filament occupies; ExtruderUsageBlcok
// (the "Blcok" typo is intentional) records the span an extruder is active in, with the start/end
// filament + logical-nozzle ids and the post-extrusion (pre-switch) partial-free sub-range. Built
// during run_post_process, consumed only by the injector side-pass under the enable_pre_heating gate.
namespace ExtruderPreHeating
{
struct FilamentUsageBlock
{
int filament_id;
int extruder_id;
int nozzle_id;
unsigned int lower_gcode_id;
unsigned int upper_gcode_id; // [lower_gcode_id,upper_gcode_id) uses current filament , upper gcode id will be set after finding next block
FilamentUsageBlock(int filament_id_, int extruder_id_, int nozzle_id_, unsigned int lower_gcode_id_, unsigned int upper_gcode_id_) :filament_id(filament_id_), extruder_id(extruder_id_), nozzle_id(nozzle_id_), lower_gcode_id(lower_gcode_id_), upper_gcode_id(upper_gcode_id_) {}
};
/**
* @brief Describle the usage of a exturder in a section
*
* The strucutre stores the start and end lines of the sections as well as
* the filament used at the beginning and end of the section.
* Post extrusion means the final extrusion before switching to the next extruder.
*
* Simplified GCode Flow:
* 1.Extruder Change Block (ext0 switch to ext1)
* 2.Extruder Usage Block (use ext1 to print)
* 3.Extruder Change Block (ext1 switch to ext0)
* 4.Extruder Usage Block (use ext0 to print)
* 5.Extruder Change Block (ext0 switch to ex1)
* ...
*
* So the construct of extruder usage block relys on two extruder change block
*/
struct ExtruderUsageBlcok
{
int extruder_id = -1;
unsigned int start_id = -1;
unsigned int end_id = -1;
int start_filament = -1;
int end_filament = -1;
int start_nozzle_id = -1;
int end_nozzle_id = -1;
unsigned int post_extrusion_start_id = -1;
unsigned int post_extrusion_end_id = -1;
bool ignore_cooling_before_tower = false;
void initialize_step_1(int extruder_id_, int start_id_, int start_filament_, int start_nozzle_id_) {
extruder_id = extruder_id_;
start_id = start_id_;
start_filament = start_filament_;
start_nozzle_id = start_nozzle_id_;
};
void initialize_step_2(int post_extrusion_start_id_) {
post_extrusion_start_id = post_extrusion_start_id_;
}
void initialize_step_3(int end_id_, int end_filament_, int post_extrusion_end_id_, int end_nozzle_id_) {
end_id = end_id_;
end_filament = end_filament_;
post_extrusion_end_id = post_extrusion_end_id_;
end_nozzle_id = end_nozzle_id_;
}
void reset() {
*this = ExtruderUsageBlcok();
}
ExtruderUsageBlcok() = default;
};
}
class CommandProcessor {
public:
@@ -347,6 +469,24 @@ class Print;
static const std::string VFlush_Start_Tag;
static const std::string VFlush_End_Tag;
static const std::string External_Purge_Tag;
public:
// Orca: SKIPPABLE region tags, stored as static strings (the FLUSH idiom above) rather than
// a CustomETags/CustomTags array. Public so the emission sites (WipeTower / change_filament
// path) can reference them single-sourced.
static const std::string Skippable_Start_Tag;
static const std::string Skippable_End_Tag;
static const std::string Skippable_Type_Tag;
// Orca: usage-block builder markers (MACHINE_START_GCODE_END / MACHINE_END_GCODE_START /
// NOZZLE_CHANGE_START / NOZZLE_CHANGE_END / CP_TOOLCHANGE_WIPE), stored as static strings (the
// FLUSH/SKIPPABLE idiom above) rather than extending the Reserved_Tags arrays — these are
// multi-nozzle markers only ever emitted by BBL-printer paths. Public so the emission sites can
// reference them single-sourced. The MACHINE_*_GCODE_* emission (GCode.cpp, gated
// enable_pre_heating) activates the usage-block builder.
static const std::string Machine_Start_GCode_End_Tag;
static const std::string Machine_End_GCode_Start_Tag;
static const std::string Nozzle_Change_Start_Tag;
static const std::string Nozzle_Change_End_Tag;
static const std::string Toolchange_Wipe_Tag;
public:
enum class ETags : unsigned char
{
@@ -455,6 +595,9 @@ class Print;
EMoveType move_type{ EMoveType::Noop };
ExtrusionRole role{ erNone };
// SKIPPABLE tag classification stamped onto each time block. Feeds skippable_part_time
// and the injector's SKIPPABLE relocation. stNone unless inside a SKIPPABLE_* region.
SkipType skippable_type{ SkipType::stNone };
unsigned int move_id{ 0 };
unsigned int g1_line_id{ 0 };
unsigned int remaining_internal_g1_lines{ 0 };
@@ -560,9 +703,24 @@ class Print;
//BBS: prepare stage time before print model, including start gcode time and mostly same with start gcode time
float prepare_time;
// Orca: extra time (e.g. a filament-change delay) that can't be attributed to a
// matching block on this pass is buffered here and retried on a later pass, so it
// is never folded into an unrelated move. On the final pass no later pass remains,
// so any still-unmatched remainder is added to the machine total (never to a move
// vertex) instead of being dropped, keeping get_time() consistent with the
// filament-change statistics. Orca-only EOF hardening; BambuStudio drops it.
using AdditionalBufferBlock = std::pair<EMoveType, float>;
using AdditionalBuffer = std::vector<AdditionalBufferBlock>;
AdditionalBuffer m_additional_time_buffer;
void reset();
void calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks = 0, float additional_time = 0.0f);
// Merge adjacent buffer entries that target the same move type.
static AdditionalBuffer merge_adjacent_additional_time_blocks(const AdditionalBuffer& buffer);
// additional_time is attributed to the first block matching target_move_type
// (EMoveType::Noop matches any block, i.e. the first processed block).
void calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks = 0, float additional_time = 0.0f, EMoveType target_move_type = EMoveType::Noop, bool is_final = false);
};
struct UsedFilaments // filaments per ColorChange
@@ -609,6 +767,25 @@ class Print;
struct TimeProcessor
{
// Orca: the insert-line taxonomy + the ordered map of lines the pre-heat/pre-cool injector
// splices into the finished g-code, keyed by output-line id. Orca keeps its single-pass
// run_post_process (M73 / filament stats / ActualSpeedMove / Backtrace /
// machine_tool_change_time) intact and applies this map in a separate, gated ADDITIVE
// second file-rewrite pass (run_second_pass_injection); with an empty map that pass is a
// byte-for-byte identity rewrite. The map is populated by the PreCoolingInjector.
enum InsertLineType
{
PlaceholderReplace,
TimePredict,
FilamentChangePredict,
ExtruderChangePredict,
PreCooling,
PreHeating,
};
// first key is line id, second key is content
using InsertedLinesMap = std::map<unsigned int, std::vector<std::pair<std::string, InsertLineType>>>;
struct Planner
{
// Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks.
@@ -636,6 +813,117 @@ class Print;
void reset();
};
// The pre-cool / pre-heat injection engine. It consumes the already-computed per-move time
// substrate (moves[i].time[valid_machine_id] / .gcode_id) and the first-pass usage blocks to
// locate idle-hotend windows, then emits M632/M400/M104/M633 lines into a
// TimeProcessor::InsertedLinesMap that the additive second file-rewrite pass
// (run_second_pass_injection) splices into the finished g-code. It is constructed and run ONLY
// when m_enable_pre_heating — single-nozzle printers (X1/P1/A1/H2S, flag false) never reach it.
// Every input is a const reference bundled from GCodeProcessor members; the injector never
// mutates GCodeProcessor state.
class PreCoolingInjector {
public:
struct ExtruderFreeBlock {
unsigned int free_lower_gcode_id;
unsigned int free_upper_gcode_id;
unsigned int partial_free_lower_id; // range of extrusion in wipe tower; without a wipe tower
unsigned int partial_free_upper_id; // partial_free lower/upper equal free_lower_gcode_id
int last_filament_id;
int next_filament_id;
int last_nozzle_id;
int next_nozzle_id;
int extruder_id; // partition key for the pre-heat/pre-cool region (extruder or hotend), not
// necessarily a real extruder id
bool ignore_cooling_before_tower = false;
};
void process_pre_cooling_and_heating(TimeProcessor::InsertedLinesMap& inserted_operation_lines);
void build_extruder_free_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks, const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
PreCoolingInjector(
const std::vector<GCodeProcessorResult::MoveVertex>& moves_,
const std::vector<std::string>& filament_types_,
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result_,
const std::vector<int>& filament_nozzle_temps_,
const std::vector<int>& filament_nozzle_temps_initial_layer_,
const std::vector<int>& physical_extruder_map_,
int valid_machine_id_,
float inject_time_threshold_,
bool handle_hotend_as_extruder_,
bool has_filament_switcher_,
const std::vector<int>& pre_cooling_temp_,
const std::vector<double>& cooling_rate_,
const std::vector<double>& heating_rate_,
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks_,
const std::vector<int>& extruder_max_nozzle_count_,
const std::vector<double>& filament_preheat_temperature_delta_,
const std::vector<double>& filament_max_temperature_drop_when_ec_,
unsigned int machine_start_gcode_end_id_,
unsigned int machine_end_gcode_start_id_,
const std::vector<ExtruderType>& extruder_types_,
const std::vector<double>& nozzle_diameter_
) :
moves(moves_),
filament_types(filament_types_),
nozzle_group_result(nozzle_group_result_),
filament_nozzle_temps(filament_nozzle_temps_),
filament_nozzle_temps_initial_layer(filament_nozzle_temps_initial_layer_),
physical_extruder_map(physical_extruder_map_),
valid_machine_id(valid_machine_id_),
inject_time_threshold(inject_time_threshold_),
handle_hotend_as_extruder(handle_hotend_as_extruder_),
has_filament_switcher(has_filament_switcher_),
filament_pre_cooling_temps(pre_cooling_temp_),
cooling_rate(cooling_rate_),
heating_rate(heating_rate_),
skippable_blocks(skippable_blocks_),
extruder_max_nozzle_count(extruder_max_nozzle_count_),
filament_preheat_temperature_delta(filament_preheat_temperature_delta_),
filament_max_temperature_drop_when_ec(filament_max_temperature_drop_when_ec_),
machine_start_gcode_end_id(machine_start_gcode_end_id_),
machine_end_gcode_start_id(machine_end_gcode_start_id_),
extruder_types(extruder_types_),
nozzle_diameter(nozzle_diameter_)
{
}
private:
std::vector<ExtruderFreeBlock> m_extruder_free_blocks;
const std::vector<GCodeProcessorResult::MoveVertex>& moves;
const std::vector<std::string>& filament_types;
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result;
const std::vector<int>& filament_nozzle_temps;
const std::vector<int>& filament_nozzle_temps_initial_layer;
const std::vector<int>& physical_extruder_map;
const int valid_machine_id;
const float inject_time_threshold;
const bool handle_hotend_as_extruder;
const bool has_filament_switcher;
const std::vector<double>& cooling_rate;
const std::vector<double>& heating_rate;
const std::vector<int>& filament_pre_cooling_temps; // target cooling temp during post extrusion
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks;
const std::vector<int>& extruder_max_nozzle_count;
const std::vector<double>& filament_preheat_temperature_delta;
const std::vector<double>& filament_max_temperature_drop_when_ec;
const unsigned int machine_start_gcode_end_id;
const unsigned int machine_end_gcode_start_id;
const std::vector<ExtruderType>& extruder_types;
const std::vector<double>& nozzle_diameter;
void inject_cooling_heating_command(
TimeProcessor::InsertedLinesMap& inserted_operation_lines,
const ExtruderFreeBlock& free_block,
float curr_temp,
float target_temp,
bool pre_cooling,
bool pre_heating
);
void build_by_filament_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks);
void build_by_extruder_blocks(const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
};
public:
class SeamsDetector
{
@@ -780,12 +1068,57 @@ class Print;
bool m_flushing; // mark a section with real flush
bool m_virtual_flushing; // mark a section with virtual flush, only for statistics
bool m_wipe_tower;
// Current-section SKIPPABLE state. Set by process_tags when inside a SKIPPABLE_* region;
// stamped onto each TimeBlock. The shipping time_lapse_gcode template emits SKIPPABLE_*
// widely, so these commonly go active (true / stTimelapse) and stamp blocks on most slices.
bool m_skippable{false};
SkipType m_skippable_type{SkipType::stNone};
int m_object_label_id{-1};
float m_print_z{0.0f};
std::vector<float> m_remaining_volume;
ExtruderTemps m_filament_nozzle_temp;
ExtruderTemps m_filament_nozzle_temp_first_layer;
std::vector<int> m_physical_extruder_map;
// Multi-nozzle context state. Per-extruder max (sub-)nozzle count; >1 marks a multi-nozzle
// extruder. Input for the pre-heat/filament-change-time injection model; not yet consumed by
// Orca's time estimator, so it is inert for existing printers.
std::vector<int> m_extruder_max_nozzle_count{1};
// Pre-heat / pre-cool injector estimator inputs. Populated from the config in apply_config
// (both overloads) and cleared in reset(), so the PreCoolingInjector has its inputs in place.
// Consumed only by the injector two-pass side-pass, gated on m_enable_pre_heating.
std::vector<std::string> m_filament_types;
std::vector<double> m_nozzle_diameter;
std::vector<double> m_hotend_cooling_rate{ 2.f };
std::vector<double> m_hotend_heating_rate{ 2.f };
std::vector<int> m_filament_pre_cooling_temp{ 0 };
std::vector<double> m_filament_preheat_temperature_delta;
bool m_enable_pre_heating{ false };
bool m_handle_hotend_as_extruder{ false };
bool m_has_filament_switcher{ false };
// [start,end] output-line-id ranges of each SKIPPABLE region, collected during
// run_post_process. The injector relocates pre-heat M104s out of these ranges. The shipping
// time_lapse_gcode template emits SKIPPABLE_* widely, so on a timelapse-on slice this is
// populated with many timelapse ranges (not empty) — the consumer must expect the common
// timelapse case, not only H2C/A2L wipe-tower ranges.
std::vector<std::pair<unsigned int, unsigned int>> m_skippable_blocks;
// First-pass usage blocks, built in run_post_process and stored on the member so the
// injector side-pass can consume them. Filled only when m_enable_pre_heating — single-nozzle
// printers (X1/P1/A1/H2S) never build them. They depend on the MACHINE_*_GCODE_* /
// NOZZLE_CHANGE_* emission the builder keys off.
std::vector<ExtruderPreHeating::FilamentUsageBlock> m_filament_blocks;
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) };
// Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each
// extruder currently carries. Written by both branches of the two-arg process_filament_change
// (the fallback branch does occupancy bookkeeping only); read by the richer change-time model
// and by the per-slot machine-limit resolution. Single-nozzle printers never populate it.
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status_recorder;
// Nozzle grouping context for slot resolution during the streaming pass. Set before the
// replay begins (see initialize_from_context); deliberately separate from
// m_result.nozzle_group_result, which is handed over only after the stream for the
// pre-heat injector's second pass and gates the richer change-time model.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
bool m_manual_filament_change;
//BBS: x, y offset for gcode generated
@@ -810,6 +1143,9 @@ class Print;
std::vector<unsigned char> m_last_filament_id;
std::vector<unsigned char> m_filament_id;
unsigned char m_extruder_id;
// Cached get_machine_config_idx() value; its inputs (active extruder + recorder occupancy)
// change only on filament-change events, where it is recomputed.
int m_machine_config_idx{0};
ExtruderColors m_extruder_colors;
ExtruderTemps m_extruder_temps;
bool m_is_XL_printer = false;
@@ -831,6 +1167,7 @@ class Print;
float m_preheat_time;
int m_preheat_steps;
bool m_disable_m73;
std::string m_printer_model;
enum class EProducer
{
@@ -860,6 +1197,11 @@ class Print;
public:
GCodeProcessor();
void init_filament_maps_and_nozzle_type_when_import_only_gcode();
// Reprocessing an already-generated g-code (from-previous / imported g-code) does not rebuild
// the per-filament nozzle grouping the multi-nozzle device GUI needs. Surface it onto the
// result: keep an already-seeded grouping (from initialize_from_context), otherwise synthesize
// a default one from the filament map so the result is never left without it.
void ensure_nozzle_group_result(int min_filament_count);
// check whether the gcode path meets the filament_map grouping requirements
bool check_multi_extruder_gcode_valid(const int extruder_size,
const Pointfs plate_printable_area,
@@ -871,6 +1213,11 @@ class Print;
const std::vector<std::set<int>>& unprintable_filament_types );
void apply_config(const PrintConfig& config);
void set_print(Print* print) { m_print = print; }
// Hand the nozzle grouping context to the estimator BEFORE the streaming replay, so the
// per-slot machine-limit resolution can follow the active nozzle. Null is fine (slot 0).
void initialize_from_context(const std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase>& nozzle_group_result) {
m_nozzle_group_result = nozzle_group_result;
}
DynamicConfig export_config_for_render() const;
@@ -1069,35 +1416,72 @@ class Print;
// Unload the current filament into the MK3 MMU2 unit at the end of print.
void process_M702(const GCodeReader::GCodeLine& line);
//Used for Elegoo printer to change tool head
void process_M6211(const GCodeReader::GCodeLine& line);
void process_elegoo_M6211(const GCodeReader::GCodeLine& line);
void process_SYNC(const GCodeReader::GCodeLine& line);
// Processes T line (Select Tool)
void process_T(const GCodeReader::GCodeLine& line);
void process_T(const std::string_view command);
// T variant carrying the H<nozzle> logical-nozzle id parsed off the command line. -1 = absent.
void process_T(const std::string_view command, int nozzle_id);
void process_M1020(const GCodeReader::GCodeLine &line);
void process_M622(const GCodeReader::GCodeLine &line);
void process_M623(const GCodeReader::GCodeLine &line);
void process_filament_change(int id);
// Richer hotend-change time model distinguishing extruder-switch / nozzle-in-extruder change /
// filament-in-nozzle change. Self-gated: for single-nozzle printers it delegates to
// process_filament_change(int) so their time estimate — hence exported g-code — is unchanged.
void process_filament_change(int id, int nozzle_id);
// Destination nozzle of a filament change: the explicit H<nozzle> id when given, else the
// filament's first nozzle in the grouping. Shared by the change-time model and the
// fallback-path occupancy bookkeeping.
std::optional<MultiNozzleUtils::NozzleInfo> resolve_target_nozzle(
const MultiNozzleUtils::NozzleGroupResultBase &group, int id, int nozzle_id) const;
// Machine slot of the nozzle currently mounted in the active extruder (0 when no grouping
// context / unknown extruder — the single-slot layout). Cached in m_machine_config_idx,
// recomputed on filament-change events.
int get_machine_config_idx() const;
// True only for multi-nozzle-capable printers (H2C cluster, or a dual/multi-extruder machine
// like H2D/X2D): the gate that admits the richer two-arg hotend-change time model. False for
// every single-extruder single-nozzle printer (X1/P1/A1/H2S/A2L).
bool use_multi_nozzle_change_time_model() const;
// post process the file with the given filename to:
// 1) add remaining time lines M73 and update moves' gcode ids accordingly
// 2) update used filament data
void run_post_process();
// Additive second file-rewrite pass. Splices the pre-heat/pre-cool injector's InsertedLinesMap
// into the finished g-code and re-shifts every move's gcode_id by the number of inserted lines
// before it. Runs only when m_enable_pre_heating, AFTER run_post_process, so single-nozzle
// printers (X1/P1/A1/H2S) never enter it; with an empty map it is a byte-for-byte identity rewrite.
void run_second_pass_injection();
// Shift each move's gcode_id by the count of injector lines inserted before it. No-op when the
// map is empty.
void handle_offsets_of_second_process(const TimeProcessor::InsertedLinesMap& inserted_operation_lines);
//BBS: different path_type is only used for arc move
void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false);
void set_extrusion_role(ExtrusionRole role);
// Resolve the SKIPPABLE_TYPE payload to a SkipType.
void set_skippable_type(const std::string_view type);
float minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
float minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
// Machine limit arrays are indexed by time mode only: [0]=Normal, [1]=Stealth.
// Do NOT add an extruder_id parameter — OrcaSlicer does not use BambuStudio's
// per-nozzle machine limits (filament_map_2 / get_config_idx_for_filament).
// Speed/acceleration limit arrays are slot-major with two mode entries per machine slot:
// [slot*2 + mode], slot from get_machine_config_idx() (0 = the only slot on single-variant
// printers, whose arrays hold just [Normal, Stealth]). The 2-arg forms read slot 0 and stay
// exactly the historical mode-only lookup; jerk and the accelerations below are mode-only.
float get_axis_max_feedrate(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
float get_axis_max_feedrate(PrintEstimatedStatistics::ETimeMode mode, Axis axis, int machine_idx) const;
float get_axis_max_acceleration(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
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;
float get_axis_max_jerk(PrintEstimatedStatistics::ETimeMode mode, Axis axis) const;
@@ -1115,10 +1499,10 @@ class Print;
void process_custom_gcode_time(CustomGCode::Type code);
void process_filaments(CustomGCode::Type code);
void calculate_time(GCodeProcessorResult& result, size_t keep_last_n_blocks = 0, float additional_time = 0.0f);
void calculate_time(GCodeProcessorResult& result, size_t keep_last_n_blocks = 0, float additional_time = 0.0f, EMoveType target_move_type = EMoveType::Noop, bool is_final = false);
// Simulates firmware st_synchronize() call
void simulate_st_synchronize(float additional_time = 0.0f);
void simulate_st_synchronize(float additional_time = 0.0f, EMoveType target_move_type = EMoveType::Noop);
void update_estimated_times_stats();
+54 -3
View File
@@ -361,7 +361,8 @@ namespace Slic3r {
* @param safe_areas A collection of extended polygons defining the safe areas.
* @return Point The nearest point within the safe areas or the default timelapse position if no safe areas exist.
*/
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision)
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision,
const std::optional<Point>& farthest_point = std::nullopt)
{
struct CandidatePoint
{
@@ -381,7 +382,11 @@ namespace Slic3r {
std::priority_queue<CandidatePoint> max_heap;
const double candidate_point_segment = scale_(5), weight_of_camera=1./3.;
auto penaltyFunc = [&weight_of_camera](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
auto penaltyFunc = [&weight_of_camera, &farthest_point](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
if (farthest_point.has_value()) {
// Farthest-point timelapse: prefer candidate closest to the farthest point (L1 norm)
return (farthest_point.value() - candidatet).cwiseAbs().sum();
}
// move distance + Camera occlusion penalty function
double ret_pen = (curr_post - candidatet).cwiseAbs().sum() - weight_of_camera * (CameraPos - candidatet).cwiseAbs().sum();
return ret_pen;
@@ -523,7 +528,7 @@ namespace Slic3r {
path_collision_area = union_ex(layer_slices_without_curr, rod_limit_areas);
}
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object);
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object, ctx.farthest_point);
}
/**
@@ -610,4 +615,50 @@ namespace Slic3r {
return *m_all_layer_pos;
}
// Whether the head can travel to X0 without crossing any other instance that is
// taller than the current print position.
bool TimelapsePosPicker::get_is_clear_to_x0(const PosPickCtx &ctx)
{
bool by_object = m_print_seq == PrintSequence::ByObject;
std::vector<const PrintObject *> object_list = get_object_list(ctx.printed_objects);
auto range_intersect = [](int left1, int right1, int left2, int right2) {
if (left1 <= left2 && left2 <= right1) return true;
if (left2 <= left1 && left1 <= right2) return true;
return false;
};
ExPolygons unclear_area;
const Layer *layer = ctx.curr_layer;
float z_target = layer->print_z;
float z_low = layer->print_z - 0.5;
float z_high = layer->print_z + 0.5;
for (auto &obj : object_list) {
for (auto &instance : obj->instances()) {
auto instance_bbox = get_real_instance_bbox(instance);
bool is_curr_obj = ( obj == object_list.back() ) || ( !by_object ),
higher_than_curr_pos = instance_bbox.max.z() > z_target;
if (!is_curr_obj && range_intersect(instance_bbox.min.z(), instance_bbox.max.z(), z_low, z_high)) {
ExPolygon expoly;
expoly.contour = {{scale_(instance_bbox.min.x()), scale_(instance_bbox.min.y())},
{scale_(instance_bbox.max.x()), scale_(instance_bbox.min.y())},
{scale_(instance_bbox.max.x()), scale_(instance_bbox.max.y())},
{scale_(instance_bbox.min.x()), scale_(instance_bbox.max.y())}};
expoly.contour = expand_object_projection(expoly.contour, by_object, higher_than_curr_pos);
unclear_area.emplace_back(std::move(expoly));
}
}
}
Point curr_pos_in_plate = {ctx.curr_pos.x() - scale_(m_plate_offset.x()), ctx.curr_pos.y() - scale_(m_plate_offset.y())};
for (const ExPolygon &expoly : unclear_area) {
BoundingBox bbox = expoly.contour.bounding_box();
if (curr_pos_in_plate.y() < bbox.min.y() || curr_pos_in_plate.y() > bbox.max.y()) continue;
if (bbox.min.x() <= curr_pos_in_plate.x()) return false;
}
return true;
}
}
@@ -22,6 +22,9 @@ namespace Slic3r {
int picture_extruder_id; // the extruder id to take picture
int curr_extruder_id;
std::optional<std::vector<const PrintObject*>> printed_objects; // printed objects, only have value in by object mode
// Farthest-point timelapse: plate-relative scaled point; when set, pick_pos_internal
// biases the picked snapshot position toward this point (nullopt → legacy camera-occlusion loss).
std::optional<Point> farthest_point;
};
// data are stored without plate offset
@@ -32,6 +35,9 @@ namespace Slic3r {
~TimelapsePosPicker() = default;
Point pick_pos(const PosPickCtx& ctx);
// Is the path to X0 clear of other (taller) instances? Drives the
// `clear_to_x0` timelapse-gcode variable (g39 clamping detection).
bool get_is_clear_to_x0(const PosPickCtx& ctx);
void init(const Print* print, const Point& plate_offset);
void reset();
private:
+711 -20
View File
@@ -7,13 +7,100 @@
namespace Slic3r
{
// ==================== MaxFlowWithLowerBounds ====================
struct MaxFlowWithLowerBounds {
public:
void add_edge(int from, int to, int capacity);
bool bfs();
int dfs(int u, int f);
int solve(std::vector<int>& matching);
public:
std::vector<int> l_nodes;
std::vector<int> r_nodes;
std::vector<Edge> edges;
std::vector<std::vector<int>> adj;
std::vector<int> level;
std::vector<int> it;
int total_nodes{ -1 };
int source_id{ -1 };
int sink_id{ -1 };
};
void MaxFlowWithLowerBounds::add_edge(int from, int to, int capacity)
{
adj[from].emplace_back(edges.size());
edges.emplace_back(from, to, capacity, 0);
// also add the reverse residual edge with zero capacity
adj[to].emplace_back(edges.size());
edges.emplace_back(to, from, 0, 0);
}
bool MaxFlowWithLowerBounds::bfs() {
level.assign(total_nodes, -1);
std::queue<int> q;
q.push(source_id);
level[source_id] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int eid : adj[u]) {
Edge &e = edges[eid];
if (e.flow < e.capacity && level[e.to] == -1) {
level[e.to] = level[u] + 1;
q.push(e.to);
}
}
}
return level[sink_id] != -1;
}
int MaxFlowWithLowerBounds::dfs(int u, int f) {
if (u == sink_id) return f;
for (int &i = it[u]; i < (int)adj[u].size(); ++i) {
int eid = adj[u][i];
Edge &e = edges[eid];
if (e.flow < e.capacity && level[e.to] == level[u] + 1) {
int pushed = dfs(e.to, std::min(f, e.capacity - e.flow));
if (pushed > 0) {
e.flow += pushed;
edges[eid ^ 1].flow -= pushed;
return pushed;
}
}
}
return 0;
}
int MaxFlowWithLowerBounds::solve(std::vector<int>& matching) {
int flow = 0;
while (bfs()) {
it.assign(total_nodes, 0);
while (int pushed = dfs(source_id, MaxFlowGraph::INF))
flow += pushed;
}
int L = l_nodes.size();
int R = r_nodes.size();
// collect l-r matches
matching.resize(l_nodes.size(), MaxFlowGraph::INVALID_ID);
for (int u = 0; u < L; ++u) {
for (int eid : adj[u]) {
Edge &e = edges[eid];
if (e.flow > 0 && e.to >= L && e.to < L + R) {
matching[e.from] = e.to - L;
}
}
}
return flow;
}
// ==================== MinCostMaxFlow ====================
struct MinCostMaxFlow {
public:
struct Edge {
int from, to, capacity, cost, flow;
Edge(int u, int v, int cap, int cst) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
};
std::vector<int> solve();
void add_edge(int from, int to, int capacity, int cost);
bool spfa(int source, int sink);
@@ -107,15 +194,10 @@ namespace Slic3r
{
if (l_nodes[idx_in_left] == -1) {
return 0;
//TODO: test more here
int sum = 0;
for (int i = 0; i < matrix.size(); ++i)
sum += matrix[i][idx_in_right];
sum /= matrix.size();
return -sum;
}
return matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
float val = matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
return std::min(static_cast<int>(val), MaxFlowGraph::MCMF_MAX_EDGE_COST);
}
@@ -123,27 +205,40 @@ namespace Slic3r
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
const std::vector<int>& u_capacity,
const std::vector<int>& v_capacity)
const std::vector<int>& v_capacity,
const std::vector<std::pair<std::set<int>,int>>& v_group_capacity)
{
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
l_nodes = u_nodes;
r_nodes = v_nodes;
total_nodes = u_nodes.size() + v_nodes.size() + 2;
total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
source_id = total_nodes - 2;
sink_id = total_nodes - 1;
adj.resize(total_nodes);
std::vector<int>v_node_to(v_nodes.size(), sink_id);
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
for (auto vid : v_group_capacity[gid].first)
v_node_to[vid] = l_nodes.size() + r_nodes.size() + gid;
}
// add edge from source to left nodes
for (int idx = 0; idx < l_nodes.size(); ++idx) {
int capacity = u_capacity.empty() ? 1 : u_capacity[idx];
add_edge(source_id, idx, capacity);
}
// add edge from right nodes to sink node
// add edge from right nodes to v_node_to(sink node or temp group node)
for (int idx = 0; idx < r_nodes.size(); ++idx) {
int capacity = v_capacity.empty() ? 1 : v_capacity[idx];
add_edge(l_nodes.size() + idx, sink_id, capacity);
add_edge(l_nodes.size() + idx, v_node_to[idx], capacity);
}
// add edge from temp group node to sink node
for (int idx = 0; idx < v_group_capacity.size(); ++idx) {
int capacity = v_group_capacity[idx].second;
add_edge(l_nodes.size() + r_nodes.size() + idx, sink_id, capacity);
}
// add edge from left nodes to right nodes
@@ -269,6 +364,301 @@ namespace Slic3r
return m_solver->solve();
}
// ==================== GeneralMinCostLowerBoundsSolver ====================
GeneralMinCostLowerBoundsSolver::~GeneralMinCostLowerBoundsSolver() = default;
GeneralMinCostLowerBoundsSolver::GeneralMinCostLowerBoundsSolver(const std::vector<FlushMatrix> &matrix_,
const std::vector<int> &u_nodes,
const std::vector<int> &v_nodes,
const std::vector<int> &v_nodes_group,
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
{
flush_matrix = matrix_;
l_nodes = u_nodes;
r_nodes = v_nodes;
r_nodes_group = v_nodes_group;
m_uv_link_limits = uv_link_limits;
m_uv_unlink_limits = uv_unlink_limits;
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
m_solver_lower_bounds = std::make_unique<MaxFlowWithLowerBounds>();
m_solver_min_cost = std::make_unique<MinCostMaxFlow>();
}
std::vector<int> GeneralMinCostLowerBoundsSolver::solve()
{
// group nodes that do not need a lower-bound constraint
std::unordered_set<int> no_lower_group;
for (int i = 0; i < r_nodes.size(); i++) {
if (r_nodes[i] >= 0)
no_lower_group.insert(r_nodes_group[i]);
}
// 1. build the lower-bound network graph
build_feasible_graph(no_lower_group);
// 2. compute the max flow
int need = 0;
for (int d : demand)
if (d > 0) need += d;
std::vector<int> feasible_matching;
int pushed_flow = m_solver_lower_bounds->solve(feasible_matching);
assert(need == pushed_flow);
// 3. convert the lower-bound max-flow network into a min-cost-max-flow network
build_graph_with_feasible_result();
// 4. compute the min-cost max-flow
auto min_cost_matching = m_solver_min_cost->solve();
return min_cost_matching;
}
void GeneralMinCostLowerBoundsSolver::build_feasible_graph(const std::unordered_set<int> &no_lower_groups)
{
m_solver_lower_bounds->l_nodes = l_nodes;
m_solver_lower_bounds->r_nodes = r_nodes;
m_solver_lower_bounds->total_nodes = l_nodes.size() + r_nodes.size() + num_groups + 2;
m_solver_lower_bounds->source_id = m_solver_lower_bounds->total_nodes - 2;
m_solver_lower_bounds->sink_id = m_solver_lower_bounds->total_nodes - 1;
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
demand.resize(m_solver_lower_bounds->total_nodes, 0);
const int L = m_solver_lower_bounds->l_nodes.size();
const int R = m_solver_lower_bounds->r_nodes.size();
// source -> l
for (int i = 0; i < L; ++i)
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->source_id, i, 1);
// u -> v (with link/unlink limits)
for (int i = 0; i < L; ++i) {
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
for (int j : it->second)
m_solver_lower_bounds->add_edge(i, L + j, 1);
continue;
}
std::optional<std::vector<int>> unlink_limits;
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
unlink_limits = it->second;
for (int j = 0; j < R; ++j) {
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
continue;
m_solver_lower_bounds->add_edge(i, L + j, 1);
}
}
// r -> group
for (int j = 0; j < R; ++j) {
int g = r_nodes_group[j];
m_solver_lower_bounds->add_edge(L + j, L + R + g, 1);
}
// group -> sink (lower bound = 1)
for (int g = 0; g < num_groups; ++g) {
if (no_lower_groups.count(g))
m_solver_lower_bounds->add_edge(L + R + g, m_solver_lower_bounds->sink_id, R);
else
add_edge_with_lower_bound(L + R + g, m_solver_lower_bounds->sink_id, 1, R, 0);
}
max_flow_edges = m_solver_lower_bounds->edges.size();
// support lower bounds, add super source super sink
super_source = m_solver_lower_bounds->total_nodes++;
super_sink = m_solver_lower_bounds->total_nodes++;
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
demand.resize(m_solver_lower_bounds->total_nodes, 0);
for (int i = 0; i < super_source; ++i) {
if (demand[i] > 0) {
m_solver_lower_bounds->add_edge(super_source, i, demand[i]);
} else if (demand[i] < 0) {
m_solver_lower_bounds->add_edge(i, super_sink, -demand[i]);
}
}
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->sink_id, m_solver_lower_bounds->source_id, MaxFlowGraph::INF);
source_id = m_solver_lower_bounds->source_id;
sink_id = m_solver_lower_bounds->sink_id;
m_solver_lower_bounds->source_id = super_source;
m_solver_lower_bounds->sink_id = super_sink;
}
void GeneralMinCostLowerBoundsSolver::build_graph_with_feasible_result()
{
for (auto&lb:lower_bound_edges){
m_solver_lower_bounds->edges[lb.edge_id].flow += lb.lower;
m_solver_lower_bounds->edges[lb.edge_id ^ 1].flow -= lb.lower;
}
m_solver_min_cost->l_nodes = m_solver_lower_bounds->l_nodes;
m_solver_min_cost->r_nodes = m_solver_lower_bounds->r_nodes;
m_solver_min_cost->source_id = source_id;
m_solver_min_cost->sink_id = sink_id;
m_solver_min_cost->total_nodes = sink_id + 1;
m_solver_min_cost->edges = m_solver_lower_bounds->edges;
m_solver_min_cost->edges.erase(m_solver_min_cost->edges.begin() + max_flow_edges, m_solver_min_cost->edges.end());
m_solver_min_cost->adj = m_solver_lower_bounds->adj;
m_solver_min_cost->adj.resize(m_solver_min_cost->total_nodes);
for (auto &node_edges : m_solver_min_cost->adj) {
node_edges.erase(std::remove_if(node_edges.begin(), node_edges.end(), [this](int val) {return val >= this->max_flow_edges;}), node_edges.end());
}
for (auto& e : m_solver_min_cost->edges) {
int L = m_solver_min_cost->l_nodes.size();
int R = m_solver_min_cost->r_nodes.size();
if (e.from < L && e.to >= L && e.to < L + R) {
int idx_in_left = e.from;
int idx_in_right = e.to - L;
int group_id = r_nodes_group[idx_in_right];
if (r_nodes[idx_in_right] == -1) continue;
e.cost = flush_matrix[group_id][l_nodes[idx_in_left]][r_nodes[idx_in_right]];
}
}
}
void GeneralMinCostLowerBoundsSolver::add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost)
{
int eid = m_solver_lower_bounds->edges.size();
m_solver_lower_bounds->add_edge(from, to, upper - lower);
lower_bound_edges.push_back({eid, lower});
demand[from] -= lower;
demand[to] += lower;
}
// ==================== GroupMinCostFlowSolver ====================
GroupMinCostFlowSolver::~GroupMinCostFlowSolver() = default;
GroupMinCostFlowSolver::GroupMinCostFlowSolver(const std::vector<FlushMatrix> &matrix_,
const std::vector<int> &u_nodes,
const std::vector<int> &v_nodes,
const std::vector<int> &v_nodes_group,
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
{
flush_matrix = matrix_;
l_nodes = u_nodes;
r_nodes = v_nodes;
r_nodes_group = v_nodes_group;
m_uv_link_limits = uv_link_limits;
m_uv_unlink_limits = uv_unlink_limits;
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
m_solver = std::make_unique<MinCostMaxFlow>();
build_graph();
}
int GroupMinCostFlowSolver::get_flush_cost(int l_idx, int r_idx)
{
if (r_nodes[r_idx] == -1)
return 0;
int group_id = r_nodes_group[r_idx];
return (int)flush_matrix[group_id][l_nodes[l_idx]][r_nodes[r_idx]];
}
void GroupMinCostFlowSolver::build_graph()
{
const int L = (int)l_nodes.size();
const int R = (int)r_nodes.size();
const int G = num_groups;
m_solver->l_nodes = l_nodes;
m_solver->r_nodes = r_nodes;
m_solver->total_nodes = L + R + G + 2;
m_solver->source_id = L + R + G;
m_solver->sink_id = L + R + G + 1;
m_solver->adj.resize(m_solver->total_nodes);
int max_flush = 0;
for (const auto &mat : flush_matrix)
for (const auto &row : mat)
for (float v : row)
max_flush = std::max(max_flush, (int)v);
int bonus = max_flush * L + 1;
// source -> l_i
for (int i = 0; i < L; ++i)
m_solver->add_edge(m_solver->source_id, i, 1, 0);
// l_i -> r_j (with link/unlink limits)
for (int i = 0; i < L; ++i) {
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
for (int j : it->second)
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
continue;
}
std::optional<std::vector<int>> unlink_limits;
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
unlink_limits = it->second;
for (int j = 0; j < R; ++j) {
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
continue;
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
}
}
// r_j -> group_g
// Compute per-nozzle incoming edge count as capacity upper bound.
// When unlink_limits restrict multiple filaments to the same nozzle,
// capacity=1 would block valid assignments. Using the actual in-degree
// allows the necessary flow while still preserving nozzle-level balance
// (a nozzle with fewer forced filaments keeps a tighter cap).
// The first unit carries a small nozzle-bonus to encourage spreading
// filaments across distinct nozzles within the same group.
int nozzle_bonus = max_flush + 1;
std::vector<int> r_in_degree(R, 0);
for (int i = 0; i < L; ++i) {
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
for (int j : it->second)
r_in_degree[j]++;
continue;
}
std::optional<std::vector<int>> unlink_limits;
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
unlink_limits = it->second;
for (int j = 0; j < R; ++j) {
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
continue;
r_in_degree[j]++;
}
}
for (int j = 0; j < R; ++j) {
int g = r_nodes_group[j];
int cap = std::max(r_in_degree[j], 1);
// First unit gets -nozzle_bonus to prefer using distinct nozzles
m_solver->add_edge(L + j, L + R + g, 1, -nozzle_bonus);
if (cap > 1)
m_solver->add_edge(L + j, L + R + g, cap - 1, 0);
}
// group_g -> sink (split: first unit gets -bonus, rest gets 0)
// bonus >> nozzle_bonus, so group coverage always takes priority
for (int g = 0; g < G; ++g) {
m_solver->add_edge(L + R + g, m_solver->sink_id, 1, -bonus);
if (L > 1)
m_solver->add_edge(L + R + g, m_solver->sink_id, L - 1, 0);
}
}
std::vector<int> GroupMinCostFlowSolver::solve()
{
return m_solver->solve();
}
// ==================== MinFlushFlowSolver ====================
MinFlushFlowSolver::~MinFlushFlowSolver()
{
}
@@ -277,7 +667,8 @@ namespace Slic3r
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
const std::vector<int>& u_capacity,
const std::vector<int>& v_capacity)
const std::vector<int>& v_capacity,
const std::vector<std::pair<std::set<int>,int>>&v_group_capacity)
{
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
@@ -286,13 +677,19 @@ namespace Slic3r
m_solver->l_nodes = u_nodes;
m_solver->r_nodes = v_nodes;
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + 2;
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
m_solver->source_id =m_solver->total_nodes - 2;
m_solver->sink_id = m_solver->total_nodes - 1;
m_solver->adj.resize(m_solver->total_nodes);
std::vector<int> v_node_to(v_nodes.size(), m_solver->sink_id);
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
for (auto vid : v_group_capacity[gid].first)
v_node_to[vid] = m_solver->l_nodes.size() + m_solver->r_nodes.size() + gid;
}
// add edge from source to left nodes,cost to 0
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
int capacity = u_capacity.empty() ? 1 : u_capacity[i];
@@ -301,7 +698,12 @@ namespace Slic3r
// add edge from right nodes to sink,cost to 0
for (int i = 0; i < m_solver->r_nodes.size(); ++i) {
int capacity = v_capacity.empty() ? 1 : v_capacity[i];
m_solver->add_edge(m_solver->l_nodes.size() + i, m_solver->sink_id, capacity, 0);
m_solver->add_edge(m_solver->l_nodes.size() + i, v_node_to[i], capacity, 0);
}
// add edge from temp group node to sink node
for(int i=0;i<v_group_capacity.size();++i){
int capacity = v_group_capacity[i].second;
m_solver->add_edge(m_solver->l_nodes.size() + m_solver->r_nodes.size() + i, m_solver->sink_id, capacity, 0);
}
// add edge from left node to right nodes
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
@@ -602,12 +1004,125 @@ namespace Slic3r
}
// Single-nozzle flush-minimizing reorder over one filament set / one flush matrix, with an
// optional seed filament. Extracted from the group loop so the multi-nozzle reorder can call it
// per physical nozzle.
// TODO: add custom sequence
static int reorder_filaments_for_minimum_flush_volume_base(const std::vector<unsigned int>& filament_lists,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const FlushMatrix& flush_matrix,
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
std::vector<std::vector<unsigned int>>* filament_sequences,
std::optional<unsigned int> initial_filament_id = std::nullopt)
{
constexpr int max_n_with_forcast = 5;
using uint128_t = boost::multiprecision::uint128_t;
if (filament_sequences) {
filament_sequences->clear();
filament_sequences->reserve(layer_filaments.size());
}
auto filament_list_to_hash_key = [](const std::vector<unsigned int>& curr_layer_filaments, const std::vector<unsigned int>& next_layer_filaments,
const std::optional<unsigned int>& prev_filament, bool use_forcast) -> uint128_t {
uint128_t hash_key = 0;
// 31-0 bit define current layer extruder,63-32 bit define next layer extruder,95~64 define prev extruder
if (prev_filament) hash_key |= (uint128_t(1) << (64 + *prev_filament));
if (use_forcast) {
for (auto item : next_layer_filaments) { hash_key |= (uint128_t(1) << (32 + item)); }
}
for (auto item : curr_layer_filaments) { hash_key |= (uint128_t(1) << item); }
return hash_key;
};
int cost = 0;
std::map<size_t, std::vector<unsigned int>> custom_layer_sequence_map;
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
std::unordered_set<unsigned int> filament_sets(filament_lists.begin(), filament_lists.end());
std::optional<unsigned int> curr_filament_id;
// use the provided initial filament id as the starting state when it is valid
if (initial_filament_id.has_value() && *initial_filament_id < flush_matrix.size()) {
curr_filament_id = initial_filament_id;
}
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
const auto& curr_lf = layer_filaments[layer];
std::vector<int> custom_filament_seq;
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
std::vector<unsigned int> unsign_custom_extruder_seq;
for (int extruder : custom_filament_seq) {
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
if (it != layer_filaments[layer].end())
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
}
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
}
}
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
const auto& curr_lf = layer_filaments[layer];
if(auto iter = custom_layer_sequence_map.find(layer); iter != custom_layer_sequence_map.end()){
auto sequence_in_group = collect_filaments_in_groups<unsigned int>(std::unordered_set<unsigned int>(filament_lists.begin(),filament_lists.end()), iter->second);
std::optional<unsigned int> prev = curr_filament_id;
for (auto& f: sequence_in_group){
if(prev)
cost += flush_matrix[*prev][f];
prev = f;
}
if(!sequence_in_group.empty()){
curr_filament_id = sequence_in_group.back();
}
if(filament_sequences)
filament_sequences->emplace_back(sequence_in_group);
continue;
}
std::vector<unsigned int> filament_used = collect_filaments_in_groups<unsigned int>(filament_sets, curr_lf);
std::vector<unsigned int> next_lf;
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1];
std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf);
bool use_forcast = false;
float tmp_cost = 0;
std::vector<unsigned int> sequence;
uint128_t hash_key = filament_list_to_hash_key(filament_used, filament_used_next_layer, curr_filament_id, use_forcast);
if (auto iter = caches.find(hash_key); iter != caches.end()) {
tmp_cost = iter->second.first;
sequence = iter->second.second;
}
else {
sequence = get_extruders_order(flush_matrix, filament_used, filament_used_next_layer, curr_filament_id, use_forcast, &tmp_cost);
caches[hash_key] = { tmp_cost,sequence };
}
if (filament_sequences)
filament_sequences->emplace_back(sequence);
if (!sequence.empty())
curr_filament_id = sequence.back();
cost += tmp_cost;
}
return cost;
}
int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int>& filament_lists,
const std::vector<int>& filament_maps,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<FlushMatrix>& flush_matrix,
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq,
std::vector<std::vector<unsigned int>>* filament_sequences)
std::vector<std::vector<unsigned int>>* filament_sequences,
const std::unordered_map<int, int>& nozzle_status)
{
//only when layer filament num <= 5,we do forcast
constexpr int max_n_with_forcast = 5;
@@ -670,6 +1185,12 @@ namespace Slic3r
if (groups[idx].empty())
continue;
std::optional<unsigned int>current_extruder_id;
// seed the group (nozzle) with the filament already loaded, if nozzle_status supplies one
if (auto it = nozzle_status.find(static_cast<int>(idx)); it != nozzle_status.end() && it->second >= 0) {
unsigned int initial_fil = static_cast<unsigned int>(it->second);
if (initial_fil < flush_matrix[idx].size())
current_extruder_id = initial_fil;
}
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
@@ -775,4 +1296,174 @@ namespace Slic3r
return cost;
}
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<FlushMatrix>& flush_matrix,
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
std::vector<std::vector<unsigned int>>* filament_sequences,
const MultiNozzleUtils::NozzleStatusRecorder& initial_status)
{
std::map<int,std::set<unsigned int>> nozzle_filament_groups;
std::map<int,std::set<int>> extruder_to_nozzle;
for(auto filament_idx : filament_lists){
auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
if (!nozzle_info)
continue;
nozzle_filament_groups[nozzle_info->group_id].insert(filament_idx);
extruder_to_nozzle[nozzle_info->extruder_id].insert(nozzle_info->group_id);
}
std::map<size_t, std::vector<unsigned int>>custom_layer_sequence_map;// save the filament sequences of custom layer
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
const auto& curr_lf = layer_filaments[layer];
std::vector<int> custom_filament_seq;
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
std::vector<unsigned int> unsign_custom_extruder_seq;
for (int extruder : custom_filament_seq) {
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
if (it != layer_filaments[layer].end())
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
}
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
}
}
std::map<int, std::vector<std::vector<unsigned int>>> nozzle_filament_sequences;
bool store_sequence = filament_sequences != nullptr;
int cost = 0;
for(auto& group : nozzle_filament_groups){
int nozzle_id = group.first;
auto& filament_in_nozzle = group.second;
int extruder_id = 0;
for(auto& [ext, nozzle_set] : extruder_to_nozzle){
if(nozzle_set.count(nozzle_id)){
extruder_id = ext;
break;
}
}
if(filament_in_nozzle.empty())
continue;
std::vector<unsigned int> filament_vec_in_nozzle(filament_in_nozzle.begin(), filament_in_nozzle.end());
int initial_fil = initial_status.get_filament_in_nozzle(nozzle_id);
std::optional<unsigned int> initial_fil_id = (initial_fil >= 0 && initial_fil < flush_matrix[extruder_id].size())? std::optional<unsigned int>(initial_fil) : std::nullopt;
std::vector<std::vector<unsigned int>> filament_seq;
cost += reorder_filaments_for_minimum_flush_volume_base(filament_vec_in_nozzle, layer_filaments, flush_matrix[extruder_id], get_custom_seq,
store_sequence ? &filament_seq : nullptr, initial_fil_id);
if(store_sequence)
nozzle_filament_sequences.emplace(nozzle_id, std::move(filament_seq));
}
if(!store_sequence)
return cost;
std::vector<int> extruders;
std::map<int, std::vector<int>> nozzles_per_extruder;
for (auto& [extruder_id, nozzle_set] : extruder_to_nozzle) {
extruders.push_back(extruder_id);
nozzles_per_extruder[extruder_id] = std::vector<int>(
nozzle_set.begin(), nozzle_set.end()
);
}
filament_sequences->clear();
filament_sequences->resize(layer_filaments.size());
// No filament in filament_lists resolved to a nozzle in nozzle_group_result
// (e.g. a degenerate input where a layer references a filament index outside the range's
// grouping map). Emit each layer's filaments in their given order so the caller still gets a
// valid per-layer sequence, and skip the cross-nozzle reorder. Guards the unchecked
// max_element(extruders) below, which would dereference end() on an empty range.
if (extruders.empty()) {
for (size_t layer = 0; layer < layer_filaments.size(); ++layer)
(*filament_sequences)[layer] = layer_filaments[layer];
return cost;
}
auto get_extruder_for_filament = [nozzle_group_result](unsigned int filament_idx) {
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
if (!nozzle)
return -1;
return nozzle->extruder_id;
};
auto get_nozzle_idx_for_filament = [nozzles_per_extruder, nozzle_group_result](unsigned int filament_idx)->int {
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
if (!nozzle)
return -1;
return std::find(nozzles_per_extruder.at(nozzle->extruder_id).begin(), nozzles_per_extruder.at(nozzle->extruder_id).end(), nozzle->group_id) - nozzles_per_extruder.at(nozzle->extruder_id).begin();
};
int initial_extruder = initial_status.get_current_extruder_id();
int last_extruder_idx = (initial_extruder >= 0 && initial_extruder < extruders.size())? initial_extruder : 0;
// set size to max extruder_id in case extruder_id is not continuous
std::vector<int> last_nozzle_idx(*std::max_element(extruders.begin(),extruders.end()) + 1,0);
for (int ext_id = 0; ext_id < static_cast<int>(last_nozzle_idx.size()); ext_id++) {
int initial_nozzle = initial_status.get_nozzle_in_extruder(ext_id);
auto ext_nozzles = nozzles_per_extruder[ext_id];
auto it = std::find(ext_nozzles.begin(), ext_nozzles.end(), initial_nozzle);
if (it != ext_nozzles.end())
last_nozzle_idx[ext_id] = static_cast<int>(std::distance(ext_nozzles.begin(), it));
}
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
auto& out_seq = (*filament_sequences)[layer];
if (custom_layer_sequence_map.find(layer) != custom_layer_sequence_map.end()) {
out_seq = custom_layer_sequence_map[layer];
if (!out_seq.empty()) {
last_extruder_idx = get_extruder_for_filament(out_seq.back());
for (auto filament : out_seq) {
int cur_ext_id = get_extruder_for_filament(filament);
last_nozzle_idx[cur_ext_id] = get_nozzle_idx_for_filament(filament);
}
}
continue;
}
if (last_extruder_idx == -1)
last_extruder_idx = 0;
int curr_last_extruder_idx = last_extruder_idx;
auto curr_last_nozzle_idx = last_nozzle_idx;
for (int i = 0; i < extruders.size(); ++i) {
int extruder_id = extruders[(last_extruder_idx + i) % extruders.size()];
auto& base_nozzles = nozzles_per_extruder[extruder_id];
bool has_seq = false;
if (last_nozzle_idx[extruder_id] == -1)
last_nozzle_idx[extruder_id] = 0;
for (int j = 0; j < base_nozzles.size(); ++j) {
int nozzle_idx = (last_nozzle_idx[extruder_id] + j) % base_nozzles.size();
int nozzle_id = base_nozzles[nozzle_idx];
const auto& frag = nozzle_filament_sequences[nozzle_id][layer];
if (frag.empty())
continue;
has_seq = true;
curr_last_nozzle_idx[extruder_id] = nozzle_idx;
out_seq.insert(out_seq.end(), frag.begin(), frag.end());
}
if (has_seq)
curr_last_extruder_idx = extruder_id;
}
last_extruder_idx = curr_last_extruder_idx;
last_nozzle_idx = curr_last_nozzle_idx;
}
return cost;
}
}
+109 -8
View File
@@ -6,7 +6,10 @@
#include <functional>
#include <limits>
#include <memory>
#include <set>
#include <unordered_set>
#include <unordered_map>
#include "../MultiNozzleUtils.hpp"
namespace Slic3r {
@@ -15,21 +18,27 @@ using FlushMatrix = std::vector<std::vector<float>>;
namespace MaxFlowGraph {
const int INF = std::numeric_limits<int>::max();
const int INVALID_ID = -1;
// Upper bound for MCMF edge cost to prevent int overflow in SPFA causing infinite loops
constexpr int MCMF_MAX_EDGE_COST = 10000000;
}
// Namespace-scope edge shared by the max-flow / min-cost-max-flow solvers below.
// The default cost keeps the plain max-flow solvers (which never read cost) source-compatible.
struct Edge
{
int from, to, capacity, cost, flow;
Edge(int u, int v, int cap, int cst = 0) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
};
class MaxFlowSolver
{
private:
struct Edge {
int from, to, capacity, flow;
Edge(int u, int v, int cap) :from(u), to(v), capacity(cap), flow(0) {}
};
public:
MaxFlowSolver(const std::vector<int>& u_nodes, const std::vector<int>& v_nodes,
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
const std::vector<int>& u_capacity = {},
const std::vector<int>& v_capacity = {}
const std::vector<int>& v_capacity = {},
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
);
std::vector<int> solve();
@@ -47,6 +56,7 @@ private:
struct MinCostMaxFlow;
struct MaxFlowWithLowerBounds;
class GeneralMinCostSolver
{
@@ -61,6 +71,84 @@ private:
std::unique_ptr<MinCostMaxFlow> m_solver;
};
class GeneralMinCostLowerBoundsSolver
{
public:
GeneralMinCostLowerBoundsSolver(
const std::vector<FlushMatrix> &matrix_,
const std::vector<int>& u_nodes,
const std::vector<int>& v_nodes,
const std::vector<int>& v_nodes_group,
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {});
std::vector<int> solve();
~GeneralMinCostLowerBoundsSolver();
private:
void build_feasible_graph(const std::unordered_set<int>& no_lower_groups);
void build_graph_with_feasible_result();
void add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost);
int get_distance(const int idx_in_left,const int idx_in_right);
private:
std::unique_ptr<MaxFlowWithLowerBounds> m_solver_lower_bounds;
std::unique_ptr<MinCostMaxFlow> m_solver_min_cost;
std::vector<FlushMatrix> flush_matrix;
std::vector<int> l_nodes;
std::vector<int> r_nodes;
std::vector<int> r_nodes_group;
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
int num_groups = 0;
// support lower bounds
struct LowerBoundEdge{
int edge_id;
int lower;
};
std::vector<int> demand;
std::vector<LowerBoundEdge> lower_bound_edges;
int super_source = -1;
int super_sink = -1;
int source_id = -1;
int sink_id = -1;
int max_flow_edges = 0;
};
class GroupMinCostFlowSolver
{
public:
GroupMinCostFlowSolver(
const std::vector<FlushMatrix> &matrix_,
const std::vector<int> &u_nodes,
const std::vector<int> &v_nodes,
const std::vector<int> &v_nodes_group,
const std::unordered_map<int, std::vector<int>> &uv_link_limits = {},
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits = {});
std::vector<int> solve();
~GroupMinCostFlowSolver();
private:
void build_graph();
int get_flush_cost(int l_idx, int r_idx);
std::unique_ptr<MinCostMaxFlow> m_solver;
std::vector<FlushMatrix> flush_matrix;
std::vector<int> l_nodes;
std::vector<int> r_nodes;
std::vector<int> r_nodes_group;
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
int num_groups = 0;
};
class MinFlushFlowSolver
{
@@ -71,7 +159,8 @@ public:
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
const std::vector<int>& u_capacity = {},
const std::vector<int>& v_capacity = {}
const std::vector<int>& v_capacity = {},
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
);
std::vector<int> solve();
~MinFlushFlowSolver();
@@ -108,7 +197,19 @@ int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int> &
const std::vector<std::vector<unsigned int>> &layer_filaments,
const std::vector<FlushMatrix> &flush_matrix,
std::optional<std::function<bool(int, std::vector<int> &)>> get_custom_seq,
std::vector<std::vector<unsigned int>> *filament_sequences);
std::vector<std::vector<unsigned int>> *filament_sequences,
const std::unordered_map<int, int>& nozzle_status = {});
// Order filaments within a per-nozzle grouping result (multi-nozzle extruders). Threads a
// NozzleStatusRecorder describing the initial physical nozzle occupancy so the reorder can reward
// keeping an already-loaded filament in place.
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<FlushMatrix>& flush_matrix,
const std::function<bool(int,std::vector<int>&)> get_custom_seq,
std::vector<std::vector<unsigned int>> * filament_sequences,
const MultiNozzleUtils::NozzleStatusRecorder& initial_status = {});
}
#endif // !TOOL_ORDER_UTILS_HPP
File diff suppressed because it is too large Load Diff
+49 -5
View File
@@ -9,6 +9,7 @@
#include <boost/container/small_vector.hpp>
#include "../FilamentGroup.hpp"
#include "../MultiNozzleUtils.hpp"
#include "../ExtrusionEntity.hpp"
#include "../PrintConfig.hpp"
@@ -98,19 +99,23 @@ private:
struct FilamentChangeStats
{
int filament_flush_weight{0};
// flush_filament_change_count counts filament changes that actually flush a physical nozzle.
// It replaces the former (dead, never populated) extruder_change_count. For single-nozzle-per-
// extruder printers it equals the per-extruder filament_change_count, so GUI stat displays are
// unchanged.
int flush_filament_change_count{0};
int filament_change_count{0};
int extruder_change_count{0};
void clear(){
filament_flush_weight = 0;
filament_change_count = 0;
extruder_change_count = 0;
flush_filament_change_count = 0;
}
FilamentChangeStats& operator+=(const FilamentChangeStats& other) {
this->filament_flush_weight += other.filament_flush_weight;
this->filament_change_count += other.filament_change_count;
this->extruder_change_count += other.extruder_change_count;
this->flush_filament_change_count += other.flush_filament_change_count;
return *this;
}
@@ -118,7 +123,7 @@ struct FilamentChangeStats
FilamentChangeStats ret;
ret.filament_flush_weight = this->filament_flush_weight + other.filament_flush_weight;
ret.filament_change_count = this->filament_change_count + other.filament_change_count;
ret.extruder_change_count = this->extruder_change_count + other.extruder_change_count;
ret.flush_filament_change_count = this->flush_filament_change_count + other.flush_filament_change_count;
return ret;
}
@@ -236,12 +241,44 @@ public:
bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().has_wipe_tower; }
int get_most_used_extruder() const { return most_used_extruder; }
// Logical (extruder, nozzle) grouping of the used filaments, built during reorder.
// For single-nozzle printers this is one logical nozzle per extruder (nozzle id == extruder id).
// Consumed by GCode (get_nozzle_id / get_first_nozzle_for_filament).
const MultiNozzleUtils::LayeredNozzleGroupResult &get_layered_nozzle_group_result() const { return m_nozzle_group_result; }
// Physical nozzle occupancy threading for the sequential (by-object) selector regroup: the
// setter seeds both the initial recorder (the state the per-layer plan starts from) and the
// running recorder (read back after sort_and_build_data via get_nozzle_status()), so each
// object's plan continues from the nozzle state the previous object ended with.
const MultiNozzleUtils::NozzleStatusRecorder &get_nozzle_status() const { return m_nozzle_status; }
void set_nozzle_status(const MultiNozzleUtils::NozzleStatusRecorder &status) { m_initial_nozzle_status = status; m_nozzle_status = status; }
/*
* called in single extruder mode, the value in map are all 0
* called in dual extruder mode, the value in map will be 0 or 1
* 0 based group id
*/
static std::vector<int> get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables);
// Nozzle-centric grouping. Returns a nozzle-aware LayeredNozzleGroupResult instead of a plain
// extruder-level std::vector<int>. Callers derive the 0/1-based extruder map via
// result.get_extruder_map(). unprintable_volumes / nozzle_status default empty for the static
// path; the per-layer engine supplies non-empty values.
static MultiNozzleUtils::LayeredNozzleGroupResult get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables, const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes = {}, const std::unordered_map<int, int>& nozzle_status = {});
// Wrap stitched per-layer filament->nozzle maps from a sequential (by-object) selector regroup
// into one print-wide result. nozzle_map_per_layer / layer_filaments / layer_sequences are the
// per-object planned layers concatenated in print order; nozzle_map_per_layer is taken by value
// and normalized in place. The nozzle list is rebuilt from the print's grouping context. Returns
// an empty result when the wrap fails. Lives here (not in Print) to reach the file-local
// grouping-context builder.
static MultiNozzleUtils::LayeredNozzleGroupResult build_sequential_group_result(
Print* print,
std::vector<std::vector<int>> nozzle_map_per_layer,
const std::vector<std::vector<unsigned int>>& layer_filaments,
const std::vector<std::vector<unsigned int>>& layer_sequences,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::set<int>>& physical_unprintables,
const std::vector<std::set<int>>& geometric_unprintables,
const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes);
// should be called after doing reorder
FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode);
@@ -283,6 +320,13 @@ private:
FilamentChangeStats m_stats_by_single_extruder;
FilamentChangeStats m_stats_by_multi_extruder_curr;
FilamentChangeStats m_stats_by_multi_extruder_best;
MultiNozzleUtils::LayeredNozzleGroupResult m_nozzle_group_result;
// Physical nozzle occupancy threaded through the per-layer selector regroup.
// m_initial_nozzle_status seeds the first combo range (empty for a fresh slice — there is no
// device continuation state); m_nozzle_status carries the running state out of the plan. Inert
// for every printer except an H2C profile that enables the filament selector (is_dynamic_group_reorder).
MultiNozzleUtils::NozzleStatusRecorder m_initial_nozzle_status;
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status;
int most_used_extruder;
};
+199 -11
View File
@@ -3,6 +3,7 @@
#include <cassert>
#include <iostream>
#include <vector>
#include <array>
#include <numeric>
#include <sstream>
#include <iomanip>
@@ -1493,6 +1494,21 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value)
{
m_flat_ironing = (m_flat_ironing && m_use_gap_wall);
// Prime-tower heating during wipe. m_is_multiple_nozzle mirrors the gate used in ToolOrdering/GCode
// (std::any_of extruder_max_nozzle_count > 1); it is false for every current printer, so the
// heating-during-wipe logic in toolchange_wipe_new is inert.
m_hotend_heating_rate = config.hotend_heating_rate.values;
m_physical_extruder_map = config.physical_extruder_map.values;
m_is_multiple_nozzle = std::any_of(config.extruder_max_nozzle_count.values.begin(),
config.extruder_max_nozzle_count.values.end(),
[](int v) { return v > 1; });
// Per-extruder printable-height clamp. Empty for single-extruder printers
// (extruder_printable_height = []), so is_valid_last_layer is inert there.
m_printable_height = config.extruder_printable_height.values;
m_last_layer_id.assign(config.nozzle_diameter.size(), -1);
// Read absolute value of first layer speed, if given as percentage,
// it is taken over following default. Speeds from config are not
// easily accessible here.
@@ -1543,6 +1559,10 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
//while (m_filpar.size() < idx+1) // makes sure the required element is in the vector
m_filpar.push_back(FilamentParameters());
// Orca: one row per filament, indexed by the raw filament id. Under a per-layer nozzle
// grouping the per-variant arrays may hold several columns per filament; the tower has no
// layer dimension here, so it keeps the filament's first column (tower x per-layer
// grouping is a documented follow-up).
m_filpar[idx].material = config.filament_type.get_at(idx);
m_filpar[idx].is_soluble = config.wipe_tower_filament == 0 ? config.filament_soluble.get_at(idx) : (idx != size_t(config.wipe_tower_filament - 1));
// BBS
@@ -1558,8 +1578,12 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
}
m_filpar[idx].tower_interface_pre_extrusion_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx);
m_filpar[idx].tower_interface_pre_extrusion_length = config.filament_tower_interface_pre_extrusion_length.get_at(idx);
// PETG pre-extrusion offset reuses the tower-interface pre-extrusion distance. Only read by the
// has_filament_switcher-gated PETG branch in get_next_pos (inert fleet-wide).
m_filpar[idx].petg_pre_extrusion_offset_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx);
m_filpar[idx].tower_ironing_area = config.filament_tower_ironing_area.get_at(idx);
m_filpar[idx].tower_interface_purge_length = config.filament_tower_interface_purge_volume.get_at(idx);
m_filpar[idx].filament_cooling_before_tower = config.filament_cooling_before_tower.get_at(idx);
// If this is a single extruder MM printer, we will use all the SE-specific config values.
// Otherwise, the defaults will be used to turn off the SE stuff.
@@ -1651,6 +1675,27 @@ Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, fl
break;
default: break;
}
// Shift the wipe start outward for a PETG pre-extrusion on filament-switcher devices, clamped to the
// shared printable bed. Gated on m_has_filament_switcher, which is false for the whole shipping fleet
// (no profile sets the key), so is_petg_pre_extrusion is always false and res is returned unchanged.
// The tower-interface contact branch is deliberately NOT applied here (enable_tower_interface_features
// DOES ship on H2C/X2D; applying it would change their g-code); is_contact_pre_extrusion is computed
// only as the guard that gives the contact path priority over PETG.
bool is_contact_pre_extrusion = interface_layer && m_enable_tower_interface_features;
bool is_petg_pre_extrusion = !is_contact_pre_extrusion && is_petg_filament(m_current_tool) && m_has_filament_switcher;
if (is_petg_pre_extrusion) {
Vec2f stop_pos = res;
float offset_dist = m_filpar[m_current_tool].petg_pre_extrusion_offset_dist;
auto printer_bbx = unscaled(get_extents(m_shared_print_bed)); // BoundingBoxBase<Vec2d>
printer_bbx.translate((-m_wipe_tower_pos - m_rib_offset).cast<double>());
if (stop_pos.x() < m_wipe_tower_width / 2.f)
stop_pos = Vec2f(stop_pos.x() - offset_dist, stop_pos.y());
else
stop_pos = Vec2f(stop_pos.x() + offset_dist, stop_pos.y());
if (stop_pos.x() < printer_bbx.min[0]) stop_pos.x() = printer_bbx.min[0];
if (stop_pos.x() > printer_bbx.max[0]) stop_pos.x() = printer_bbx.max[0];
res = stop_pos;
}
return res;
}
@@ -2645,6 +2690,11 @@ bool WipeTower::is_tpu_filament(int filament_id) const
return m_filpar[filament_id].material == "TPU";
}
bool WipeTower::is_petg_filament(int filament_id) const
{
return m_filpar[filament_id].material == "PETG";
}
// BBS: consider both soluable and support properties
// Return index of first toolchange that switches to non-soluble and non-support extruder
// ot -1 if there is no such toolchange.
@@ -2717,6 +2767,9 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer)
float spacing = m_layer_info->extra_spacing;
if (has_tpu_filament() && m_layer_info->extra_spacing < m_tpu_fixed_spacing) spacing = 1;
float nozzle_change_depth = tool_change.nozzle_change_depth * spacing;
// Drop the nozzle-change depth on an extruder's final layer above its printable height
// (inert unless is_valid_last_layer clamps, i.e. multi-extruder near Z-max).
if (!is_valid_last_layer(old_filament, m_cur_layer_id, layer.z)) nozzle_change_depth = 0.f;
//float nozzle_change_depth = tool_change.nozzle_change_depth * (has_tpu_filament() ? m_tpu_fixed_spacing : layer.extra_spacing);
auto* block = get_block_by_category(m_filpar[new_filament].category, false);
if (!block)
@@ -2760,7 +2813,10 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer)
WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool solid_toolchange,bool solid_nozzlechange)
{
m_nozzle_change_result.gcode.clear();
if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool]) {
// Skip the cross-extruder nozzle change (ramming) on an extruder's final layer above its printable
// height. is_valid_last_layer is inert unless multi-extruder near Z-max.
if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool]
&& is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) {
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
}
@@ -3411,23 +3467,103 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
x_to_wipe = solid_tool_toolchange ? std::numeric_limits<float>::max(): x_to_wipe;
float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 4800.f) : 4800.f;
target_speed = solid_tool_toolchange ? 20.f * 60.f : target_speed;
float wipe_speed = 0.33f * target_speed;
// Nominal wipe-speed schedule. The applied wipe_speed is nominal_speed * speed_factor; speed_factor
// stays 1.0 unless the H2C prime-tower heating-during-wipe model below slows the wipe so the hotend
// can reach temperature (nominal_speed == wipe_speed when speed_factor == 1, i.e. single-nozzle).
float nominal_speed = 0.33f * target_speed;
m_left_to_right = ((m_cur_layer_id + 3) % 4 >= 2);
bool is_from_up = (m_cur_layer_id % 2 == 1);
// Prime-tower heating during wipe. Everything here is gated on m_is_multiple_nozzle (false for every
// current printer); the lambdas emit nothing until add_M104_by_requirement's gate opens, so the
// single-nozzle wipe is untouched.
// WipeSpeedMap mirrors the nominal schedule above and is read only by estimate_wipe_time. It is a
// std::array (stack, no per-call heap allocation); values depend on runtime target_speed so it
// cannot be static const.
const std::array<float, 5> WipeSpeedMap{0.33f * target_speed, 0.375f * target_speed, 0.458f * target_speed,
0.875f * target_speed, std::min(target_speed, 0.875f * target_speed + 50.f)};
auto estimate_wipe_time = [&cleaning_box, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange]() -> float {
int n = std::ceil(x_to_wipe / (xr - xl));
if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy;
float one_line_len = xr - xl;
float time = std::numeric_limits<float>::max();
if (n <= 1)
time = one_line_len / WipeSpeedMap[0];
else if (n <= 2)
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1];
else if (n <= 3)
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2];
else if (n <= 4)
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3];
else {
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3];
time += (n - 4) * one_line_len / WipeSpeedMap[4];
}
return time * 60.f;
};
// Emit the arriving-hotend pre-heat inside the M632/M633 nozzle-change barrier. `M632 S<tool>[ H<nozzle>]
// M N` opens the barrier (M = firmware nozzle-change flag, N = slicer generated), the M104 sets the
// arriving hotend temp, and `M633` closes it. H2C's grouping is static (no dynamic nozzle map), so the
// H<nozzle> field is omitted (a dynamic nozzle map would supply a real nozzle id, a static map -1 => no
// H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally
// omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from
// add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C
// only; every other printer's wipe tower is untouched. The M632 M-flag is itself a firmware barrier, so
// a preceding M400 wait is subsumed.
auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") {
std::string buffer;
buffer += "M632 S" + std::to_string(m_current_tool) + " M N\n";
buffer += "M104";
if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size())
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer
if (!comment.empty()) buffer += " ;" + comment;
buffer += '\n';
buffer += "M633\n";
(void) wait_for_moves; // the M632 M-flag barrier replaces the former M400 wait
return buffer;
};
// Suppress the pre-heat M104 on the first layer and on solid (contact) toolchanges (should_heating).
// m_is_multiple_nozzle folds in the H2C gate so single-nozzle output is untouched.
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (layer-static) because Orca's
// wipe tower is extruder-level rather than tracking a per-layer nozzle map.
bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON &&
!solid_tool_toolchange && !is_first_layer();
auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() {
if (m_filpar[m_current_tool].filament_cooling_before_tower < EPSILON) return;
if (!should_heating) return;
float target_temp = is_first_layer() ? m_filpar[m_current_tool].nozzle_temperature_initial_layer : m_filpar[m_current_tool].nozzle_temperature;
writer.append(format_line_M104(target_temp, m_filament_map[m_current_tool] - 1));
};
float speed_factor = 1.f;
if (should_heating) {
// The heating-slowdown scaling is disabled — no additional heating time is required, so
// speed_factor stays 1.0. The structure and estimate_wipe_time/WipeSpeedMap are retained for
// future H2C tuning; the divide-by-zero/bounds guard is preserved in the commented body below.
// int extruder_id = m_filament_map[m_current_tool] - 1;
// if (extruder_id >= 0 && extruder_id < (int) m_hotend_heating_rate.size() && m_hotend_heating_rate[extruder_id] > 0.) {
// float estimate_time = estimate_wipe_time();
// float heat_time = m_filpar[m_current_tool].filament_cooling_before_tower / m_hotend_heating_rate[extruder_id];
// if (estimate_time < heat_time) speed_factor = estimate_time / heat_time;
// }
(void) estimate_wipe_time; // retain scaffolding above without an unused-lambda warning
}
float wipe_speed = nominal_speed * speed_factor;
// now the wiping itself:
for (int i = 0; true; ++i) {
if (i != 0) {
if (wipe_speed < 0.34f * target_speed)
wipe_speed = 0.375f * target_speed;
else if (wipe_speed < 0.377 * target_speed)
wipe_speed = 0.458f * target_speed;
else if (wipe_speed < 0.46f * target_speed)
wipe_speed = 0.875f * target_speed;
if (nominal_speed < 0.34f * target_speed)
nominal_speed = 0.375f * target_speed;
else if (nominal_speed < 0.377 * target_speed)
nominal_speed = 0.458f * target_speed;
else if (nominal_speed < 0.46f * target_speed)
nominal_speed = 0.875f * target_speed;
else
wipe_speed = std::min(target_speed, wipe_speed + 50.f);
nominal_speed = std::min(target_speed, nominal_speed + 50.f);
wipe_speed = nominal_speed * speed_factor;
}
bool need_change_flow = need_thick_bridge_flow(writer.y());
@@ -3453,6 +3589,7 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
} else
writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 240.);
writer.retract(-retract_length, retract_speed);
add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
} else {
float dx = xl - wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x();
@@ -3468,9 +3605,11 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
}else
writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 240.);
writer.retract(-retract_length, retract_speed);
add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
}
} else {
if (i == 0) add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
if (m_left_to_right)
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
else
@@ -3571,6 +3710,47 @@ bool WipeTower::is_in_same_extruder(int filament_id_1, int filament_id_2)
return m_filament_map[filament_id_1] == m_filament_map[filament_id_2];
}
// Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer,
// or is it its final layer above the extruder's printable height?
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static),
// because Orca's wipe tower is extruder-level rather than tracking a per-layer nozzle map (the same
// idiom the pre-heat path uses in toolchange_wipe_new). Gated on m_is_multi_extruder so that
// single-extruder printers (including ones whose extruder_printable_height defaults to {0}) always
// return true and leave wipe-tower g-code unchanged.
bool WipeTower::is_valid_last_layer(int tool, int layer_id, double layer_z) const
{
if (!m_is_multi_extruder)
return true;
int extruder_id = (tool >= 0 && tool < (int) m_filament_map.size()) ? m_filament_map[tool] - 1 : -1;
if (extruder_id < 0 || extruder_id >= (int) m_printable_height.size() || extruder_id >= (int) m_last_layer_id.size())
return true;
if (m_last_layer_id[extruder_id] == layer_id && layer_z > m_printable_height[extruder_id])
return false;
return true;
}
// Records, per extruder, the last wipe-tower layer index that uses it, so is_valid_last_layer can
// recognise the extruder's final layer. Inert for single-extruder printers (early return);
// bounds-checked because m_filament_map may be empty/short.
void WipeTower::set_nozzle_last_layer_id()
{
if (!m_is_multi_extruder)
return;
for (int idx = 0; idx < (int) m_plan.size(); ++idx) {
const auto &info = m_plan[idx];
for (const auto &tc : info.tool_changes) {
int old_tool = (int) tc.old_tool;
int new_tool = (int) tc.new_tool;
int old_ext = (old_tool >= 0 && old_tool < (int) m_filament_map.size()) ? m_filament_map[old_tool] - 1 : -1;
int new_ext = (new_tool >= 0 && new_tool < (int) m_filament_map.size()) ? m_filament_map[new_tool] - 1 : -1;
if (old_ext >= 0 && old_ext < (int) m_last_layer_id.size())
m_last_layer_id[old_ext] = idx;
if (new_ext >= 0 && new_ext < (int) m_last_layer_id.size())
m_last_layer_id[new_ext] = idx;
}
}
}
void WipeTower::reset_block_status()
{
for (auto &block : m_wipe_tower_blocks) {
@@ -3797,6 +3977,7 @@ void WipeTower::plan_tower_new()
}
update_all_layer_depth(max_depth);
set_nozzle_last_layer_id(); // record per-extruder last layer for is_valid_last_layer
float diagonal = sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width);
m_rib_length = std::max({m_rib_length, diagonal});
m_rib_length += m_extra_rib_length;
@@ -3916,9 +4097,12 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
int candidate_id = -1;
for (size_t idx = 0; idx < layer.tool_changes.size(); ++idx) {
if (idx == 0) {
if (layer.tool_changes[idx].old_tool == wall_filament_id)
// An extruder's last-layer filament above its printable height cannot supply the
// outer wall. is_valid_last_layer is inert unless it clamps.
if (layer.tool_changes[idx].old_tool == wall_filament_id && is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z))
return wall_filament_id;
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category) {
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category &&
is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z)) {
candidate_id = layer.tool_changes[idx].old_tool;
}
}
@@ -4017,6 +4201,10 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
finish_layer_filament = wall_idx;
}
// Cancel a block on the last layer above its extruder's printable height.
// is_valid_last_layer is inert unless multi-extruder near Z-max.
if (!is_valid_last_layer(finish_layer_filament, m_cur_layer_id, layer.z)) continue;
ToolChangeResult finish_block_tcr;
if (interface_solid || (block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament])) {
interface_solid = interface_solid && !((block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament]));//noly reduce speed when
+41
View File
@@ -313,6 +313,13 @@ public:
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
bool has_tpu_filament() const { return m_has_tpu_filament; }
// Orca: has_filament_switcher is not a static PrintConfig member, so it is pushed in from Print
// via a setter rather than read in the ctor. Device-set only.
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
// The region every extruder can reach, used to clamp the PETG pre-extrusion offset to the
// printable bed.
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
struct FilamentParameters {
std::string material = "PLA";
int category;
@@ -341,8 +348,14 @@ public:
float wipe_dist;
float tower_interface_pre_extrusion_dist = 0.f;
float tower_interface_pre_extrusion_length = 0.f;
// Outward shift of the wipe start for a PETG pre-extrusion on filament-switcher devices;
// set from filament_tower_interface_pre_extrusion_dist.
float petg_pre_extrusion_offset_dist = 0.f;
float tower_ironing_area = 4.f;
float tower_interface_purge_length = 0.f;
// Distance (in mm of filament) that a hotend is allowed to pre-cool before the
// tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
float filament_cooling_before_tower = 0.f;
};
@@ -462,6 +475,22 @@ private:
bool m_adhesion = true;
GCodeFlavor m_gcode_flavor;
// Multi-nozzle prime-tower heating during wipe. m_is_multiple_nozzle gates the whole
// feature; it is false for every current (single-nozzle) printer (extruder_max_nozzle_count
// defaults to 1), so the pre-heat/pre-cool path is inert and wipe-tower g-code is unchanged.
bool m_is_multiple_nozzle = false;
std::vector<double> m_hotend_heating_rate; // config.hotend_heating_rate (deg/s per extruder)
std::vector<int> m_physical_extruder_map; // logical extruder -> physical tool number (M104 T param)
// Per-extruder printable-height clamp. m_printable_height = config.extruder_printable_height
// (per-extruder Z limit; empty for single-extruder printers, [320,325] for H2D). m_last_layer_id
// records, per extruder, the last wipe-tower layer that uses it. is_valid_last_layer() is gated on
// m_is_multi_extruder so single-extruder wipe-tower g-code is unchanged; the clamp only bites a
// multi-extruder wipe tower whose final per-extruder layer exceeds that extruder's printable
// height (near the Z limit).
std::vector<double> m_printable_height;
std::vector<int> m_last_layer_id;
// Bed properties
enum {
RectangularBed,
@@ -501,6 +530,11 @@ private:
bool m_flat_ironing=false;
bool m_enable_tower_interface_features=false;
bool m_enable_tower_interface_cooldown_during_tower=false;
// Filament-switcher device flag + shared printable bed for the PETG pre-extrusion offset.
// m_has_filament_switcher is false for the whole shipping fleet (no profile sets the key), so
// the PETG branch in get_next_pos never runs -> no change fleet-wide.
bool m_has_filament_switcher=false;
Polygons m_shared_print_bed;
bool m_prev_layer_had_interface=false;
bool m_current_layer_has_interface=false;
// Calculates length of extrusion line to extrude given volume
@@ -520,6 +554,7 @@ private:
void save_on_last_wipe();
bool is_tpu_filament(int filament_id) const;
bool is_petg_filament(int filament_id) const;
// BBS
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
@@ -586,6 +621,12 @@ private:
const box_coordinates &cleaning_box,
float wipe_volume);
void get_wall_skip_points(const WipeTowerInfo &layer);
// Per-extruder printable-height clamp (see m_printable_height). is_valid_last_layer returns
// false only for a multi-extruder wipe tower's final per-extruder layer that exceeds that
// extruder's printable height; returns true (no clamp) in every other case.
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
void set_nozzle_last_layer_id();
};
+4
View File
@@ -1333,6 +1333,10 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config)
//while (m_filpar.size() < idx+1) // makes sure the required element is in the vector
m_filpar.push_back(FilamentParameters());
// Orca: one row per filament, indexed by the raw filament id. Under a per-layer nozzle
// grouping the per-variant arrays may hold several columns per filament; the tower has no
// layer dimension here, so it keeps the filament's first column (tower x per-layer
// grouping is a documented follow-up).
m_filpar[idx].material = config.filament_type.get_at(idx);
if (m_wipe_tower_filament > 0)
m_filpar[idx].is_soluble = (idx != size_t(m_wipe_tower_filament - 1));
+54 -30
View File
@@ -110,6 +110,7 @@ void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
m_filament_extruders.clear();
//ORCA: Reset current extruder ID and clear pointers to prevent dangling pointers when extruders are recreated.
m_curr_extruder_id = -1;
m_cached_extruder_idx = 0;
std::fill(m_curr_filament_extruder.begin(), m_curr_filament_extruder.end(), nullptr);
m_filament_extruders.reserve(extruder_ids.size());
for (unsigned int extruder_id : extruder_ids)
@@ -594,36 +595,36 @@ std::string GCodeWriter::update_progress(unsigned int num, unsigned int tot, boo
std::string GCodeWriter::toolchange_prefix() const
{
std::string gcode = "T";
// Orca: the manual-filament-change tag must stay ahead of the flavor selection so
// MMU manual-change handling keeps working.
if (config.manual_filament_change)
gcode = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Manual_Tool_Change) + "T";
else {
if (m_is_bbl_printers)
gcode = "M1020 S";
else {
if (FLAVOR_IS(gcfMakerWare))
gcode = "M135 T";
else if (FLAVOR_IS(gcfSailfish))
gcode = "M108 T";
}
}
return gcode;
return ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Manual_Tool_Change) + "T";
return FLAVOR_IS(gcfMakerWare) ? "M135 T" :
FLAVOR_IS(gcfSailfish) ? "M108 T" : "T";
}
std::string GCodeWriter::toolchange(unsigned int filament_id)
std::string GCodeWriter::toolchange(unsigned int filament_id, int nozzle_id)
{
// set the new extruder
auto filament_extruder_iter = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(), [filament_id](const Extruder &e) { return e.id() < filament_id; });
assert(filament_extruder_iter != m_filament_extruders.end() && filament_extruder_iter->id() == filament_id);
m_curr_extruder_id = filament_extruder_iter->extruder_id();
m_curr_filament_extruder[m_curr_extruder_id] = &*filament_extruder_iter;
m_cached_extruder_idx = get_extruder_index(this->config, filament_id);
// return the toolchange command
// if we are running a single-extruder setup, just set the extruder and return nothing
std::ostringstream gcode;
// Orca: also emit for non-BBL single-extruder multi-filament setups (MMU-style).
if (this->multiple_extruders || (this->config.filament_diameter.values.size() > 1 && !is_bbl_printers())) {
// Orca: call toolchange_prefix() to get the correct command prefix based on the configuration and flavor.
gcode << this->toolchange_prefix() << filament_id;
// Orca: manual filament change keeps its tag line even on BBL machines, so the
// M1020 form must not shadow it. nozzle_id is signed: the null-safe nozzle
// lookup legitimately yields -1 ("no specific nozzle"), matching the literal
// H-1 the stock change templates emit; an unsigned would wrap.
if (m_is_bbl_printers && !config.manual_filament_change)
gcode << "M1020 S" << filament_id << " H" << nozzle_id;
else
gcode << this->toolchange_prefix() << filament_id;
if (GCodeWriter::full_gcode_comment)
gcode << " ; change extruder";
gcode << "\n";
@@ -632,6 +633,25 @@ std::string GCodeWriter::toolchange(unsigned int filament_id)
return gcode.str();
}
// Current parked-retract length of the filament's extruder, share-aware. m_filament_extruders is
// sorted by id (see toolchange), so a lower_bound lookup finds the entry; unknown filament ids
// degrade to 0 rather than dereferencing end().
double GCodeWriter::get_extruder_retracted_length(const int filament_id)
{
double res = 0.0;
auto filament_extruder_iter = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(),
[filament_id](const Extruder &e) { return (int) e.id() < filament_id; });
if (filament_extruder_iter == m_filament_extruders.end() || (int) filament_extruder_iter->id() != filament_id)
return res;
if (filament_extruder_iter->is_share_extruder())
res = filament_extruder_iter->get_share_retracted_length();
else
res = filament_extruder_iter->get_single_retracted_length();
return res;
}
std::string GCodeWriter::set_speed(double F, const std::string &comment, const std::string &cooling_marker)
{
assert(F > 0.);
@@ -658,7 +678,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com
GCodeG1Formatter w;
w.emit_xy(point_on_plate);
auto speed = m_is_first_layer
? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id())) : this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
w.emit_f(speed * 60.0);
//BBS
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
@@ -744,7 +764,7 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
// BBS
Vec3d dest_point = point;
auto travel_speed =
m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id())) : this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
//BBS: a z_hop need to be handle when travel
if (std::abs(m_to_lift) > EPSILON) {
assert(std::abs(m_lifted) < EPSILON);
@@ -841,13 +861,13 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
{
//force to move xy first then z after filament change
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
w.emit_f(this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id())) * 60.0);
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string() + _travel_to_z(point_on_plate.z(), comment);
} else {
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
w.emit_f(this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id())) * 60.0);
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string();
}
@@ -880,10 +900,10 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
{
m_pos(2) = z;
double speed = this->config.travel_speed_z.get_at(get_extruder_index(this->config, filament()->id()));
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id()))
: this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
GCodeG1Formatter w;
@@ -897,11 +917,11 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment)
{
std::string output;
double speed = this->config.travel_speed_z.get_at(get_extruder_index(this->config, filament()->id()));
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", get_extruder_index(this->config, filament()->id()))
: this->config.travel_speed.get_at(get_extruder_index(this->config, filament()->id()));
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
if (!this->config.enable_arc_fitting) { // Orca: if arc fitting is disabled, approximate the arc with small linear segments
@@ -1101,7 +1121,7 @@ std::string GCodeWriter::_retract(double length, double restart_extra, const std
return gcode;
}
std::string GCodeWriter::unretract()
std::string GCodeWriter::unretract(float extra_retract)
{
std::string gcode;
@@ -1117,7 +1137,9 @@ std::string GCodeWriter::unretract()
//BBS
// use G1 instead of G0 because G0 will blend the restart with the previous travel move
GCodeG1Formatter w;
w.emit_e(filament()->E());
// extra_retract over-extrudes for the PETG pre-extrusion; 0 by
// default -> identical to the plain deretract E position.
w.emit_e(filament()->E() + extra_retract);
w.emit_f(filament()->deretract_speed() * 60.);
//BBS
w.emit_comment(GCodeWriter::full_gcode_comment, " ; unretract");
@@ -1250,8 +1272,9 @@ std::string GCodeWriter::set_extruder(unsigned int filament_id)
auto filament_ext_it = Slic3r::lower_bound_by_predicate(m_filament_extruders.begin(), m_filament_extruders.end(), [filament_id](const Extruder &e) { return e.id() < filament_id; });
unsigned int extruder_id = filament_ext_it->extruder_id();
assert(filament_ext_it != m_filament_extruders.end() && filament_ext_it->id() == filament_id);
//TODO: optmize here, pass extruder_id to toolchange
return this->need_toolchange(filament_id) ? this->toolchange(filament_id) : "";
// Orca: writer-only context (calibration paths) has no nozzle grouping; the
// filament's own extruder id is the correct degenerate nozzle value.
return this->need_toolchange(filament_id) ? this->toolchange(filament_id, (int) extruder_id) : "";
}
void GCodeWriter::init_extruder(unsigned int filament_id)
@@ -1261,6 +1284,7 @@ void GCodeWriter::init_extruder(unsigned int filament_id)
assert(filament_extruder_iter != m_filament_extruders.end() && filament_extruder_iter->id() == filament_id);
m_curr_extruder_id = filament_extruder_iter->extruder_id();
m_curr_filament_extruder[m_curr_extruder_id] = &*filament_extruder_iter;
m_cached_extruder_idx = get_extruder_index(this->config, filament_id);
}
}
+10 -2
View File
@@ -19,6 +19,7 @@ public:
GCodeWriter() :
multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_cached_extruder_idx(0),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
@@ -66,10 +67,13 @@ public:
bool need_toolchange(unsigned int filament_id) const;
std::string set_extruder(unsigned int filament_id);
void init_extruder(unsigned int filament_id);
// Current parked-retract length of a filament's extruder (share-aware). Used for the
// new_extruder_retracted_length change-filament placeholder. Returns 0 if the filament is unknown.
double get_extruder_retracted_length(const int filament_id);
// Prefix of the toolchange G-code line, to be used by the CoolingBuffer to separate sections of the G-code
// printed with the same extruder.
std::string toolchange_prefix() const;
std::string toolchange(unsigned int filament_id);
std::string toolchange(unsigned int filament_id, int nozzle_id);
std::string set_speed(double F, const std::string &comment = std::string(), const std::string &cooling_marker = std::string());
// SoftFever NOTE: the returned speed is mm/minute
double get_current_speed() const { return m_current_speed;}
@@ -83,7 +87,9 @@ public:
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
std::string retract(bool before_wipe = false, double retract_length = 0);
std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0);
std::string unretract();
// extra_retract adds a small over-extrusion to the deretract move (PETG pre-extrusion).
// Default 0 -> byte-identical to the plain deretract.
std::string unretract(float extra_retract = 0.f);
// do lift instantly
std::string eager_lift(const LiftType type);
// record a lift request, do realy lift in next travel
@@ -135,6 +141,8 @@ public:
bool m_single_extruder_multi_material;
std::vector<Extruder*> m_curr_filament_extruder;
int m_curr_extruder_id;
// Motion uses the global/base process variant until a filament becomes active.
size_t m_cached_extruder_idx;
unsigned int m_last_acceleration;
unsigned int m_last_travel_acceleration;
std::vector<unsigned int> m_max_travel_acceleration;
+1
View File
@@ -149,6 +149,7 @@ bool Layer::is_perimeter_compatible(const Print& print, const PrintRegion& a, co
&& config.inner_wall_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.inner_wall_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.outer_wall_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.outer_wall_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.small_perimeter_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.small_perimeter_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.small_support_perimeter_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.small_support_perimeter_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value
&& config.detect_overhang_wall == other_config.detect_overhang_wall
+4
View File
@@ -157,6 +157,10 @@ public:
ExPolygons lslices;
ExPolygons lslices_extrudable; // BBS: the extrudable part of lslices used for tree support
std::vector<BoundingBox> lslices_bboxes;
// Orca: for separated infills / per-model centering. Aligned with lslices: for each island, the
// full bounding box of the 3D connected body (across all layers) it belongs to. Populated by
// PrintObject::infill() only when the feature is used; empty otherwise.
std::vector<BoundingBox> lslices_separated_component_bboxes;
// BBS
ExPolygons loverhangs;
+8 -5
View File
@@ -187,8 +187,8 @@ Model Model::read_from_step(const std::string&
ImportStepProgressFn stepFn,
StepIsUtf8Fn stepIsUtf8Fn,
std::function<int(Slic3r::Step&, double&, double&, bool&)> step_mesh_fn,
double linear_defletion,
double angle_defletion,
double linear_deflection,
double angle_deflection,
bool is_split_compound)
{
Model model;
@@ -201,13 +201,13 @@ Model Model::read_from_step(const std::string&
goto _finished;
}
if (step_mesh_fn) {
if (step_mesh_fn(step_file, linear_defletion, angle_defletion, is_split_compound) == -1) {
if (step_mesh_fn(step_file, linear_deflection, angle_deflection, is_split_compound) == -1) {
status = Step::Step_Status::CANCEL;
goto _finished;
}
}
status = step_file.mesh(&model, is_cb_cancel, is_split_compound, linear_defletion, angle_defletion);
status = step_file.mesh(&model, is_cb_cancel, is_split_compound, linear_deflection, angle_deflection);
_finished:
@@ -3226,6 +3226,7 @@ double Model::findMaxSpeed(const ModelObject* object) {
double topSolidInfillSpeedObj = Model::printSpeedMap.topSolidInfillSpeed;
double supportSpeedObj = Model::printSpeedMap.supportSpeed;
double smallPerimeterSpeedObj = Model::printSpeedMap.smallPerimeterSpeed;
double smallSupportPerimeterSpeedObj = Model::printSpeedMap.smallSupportPerimeterSpeed;
for (std::string objectKey : objectKeys) {
if (objectKey == "inner_wall_speed"){
perimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
@@ -3243,8 +3244,10 @@ double Model::findMaxSpeed(const ModelObject* object) {
externalPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
if (objectKey == "small_perimeter_speed")
smallPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
if (objectKey == "small_support_perimeter_speed")
smallSupportPerimeterSpeedObj = object->config.get().opt_float_nullable(objectKey, 0);
}
objMaxSpeed = std::max(perimeterSpeedObj, std::max(externalPerimeterSpeedObj, std::max(infillSpeedObj, std::max(solidInfillSpeedObj, std::max(topSolidInfillSpeedObj, std::max(supportSpeedObj, std::max(smallPerimeterSpeedObj, objMaxSpeed)))))));
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.;
return objMaxSpeed;
}
+20 -12
View File
@@ -920,6 +920,13 @@ public:
// Extruder ID is only valid for FFF. Returns -1 for SLA or if the extruder ID is not applicable (support volumes).
int extruder_id() const;
//Orca: cache clearing procedure to ensure that the shape is positioned accurately when manipulating it
void clear_cache() {
m_cached_trans_matrix = Transform3d::Identity().inverse(); // get unvelivable matrix
m_convex_hull_2d.clear();
m_cached_2d_polygon.clear();
};
bool is_splittable() const;
// BBS
@@ -966,34 +973,34 @@ public:
static std::string type_to_string(const ModelVolumeType t);
const Geometry::Transformation& get_transformation() const { return m_transformation; }
void set_transformation(const Geometry::Transformation& transformation) { m_transformation = transformation; }
void set_transformation(const Transform3d& trafo) { m_transformation.set_matrix(trafo); }
void set_transformation(const Geometry::Transformation& transformation) { clear_cache(); m_transformation = transformation; }
void set_transformation(const Transform3d& trafo) { clear_cache(); m_transformation.set_matrix(trafo); }
Vec3d get_offset() const { return m_transformation.get_offset(); }
double get_offset(Axis axis) const { return m_transformation.get_offset(axis); }
void set_offset(const Vec3d& offset) { m_transformation.set_offset(offset); }
void set_offset(Axis axis, double offset) { m_transformation.set_offset(axis, offset); }
void set_offset(const Vec3d& offset) { clear_cache(); m_transformation.set_offset(offset); }
void set_offset(Axis axis, double offset) { clear_cache(); m_transformation.set_offset(axis, offset); }
Vec3d get_rotation() const { return m_transformation.get_rotation(); }
double get_rotation(Axis axis) const { return m_transformation.get_rotation(axis); }
void set_rotation(const Vec3d& rotation) { m_transformation.set_rotation(rotation); }
void set_rotation(Axis axis, double rotation) { m_transformation.set_rotation(axis, rotation); }
void set_rotation(const Vec3d& rotation) { clear_cache(); m_transformation.set_rotation(rotation); }
void set_rotation(Axis axis, double rotation) { clear_cache(); m_transformation.set_rotation(axis, rotation); }
Vec3d get_scaling_factor() const { return m_transformation.get_scaling_factor(); }
double get_scaling_factor(Axis axis) const { return m_transformation.get_scaling_factor(axis); }
void set_scaling_factor(const Vec3d& scaling_factor) { m_transformation.set_scaling_factor(scaling_factor); }
void set_scaling_factor(Axis axis, double scaling_factor) { m_transformation.set_scaling_factor(axis, scaling_factor); }
void set_scaling_factor(const Vec3d& scaling_factor) { clear_cache(); m_transformation.set_scaling_factor(scaling_factor); }
void set_scaling_factor(Axis axis, double scaling_factor) {clear_cache(); m_transformation.set_scaling_factor(axis, scaling_factor); }
Vec3d get_mirror() const { return m_transformation.get_mirror(); }
double get_mirror(Axis axis) const { return m_transformation.get_mirror(axis); }
bool is_left_handed() const { return m_transformation.is_left_handed(); }
void set_mirror(const Vec3d& mirror) { m_transformation.set_mirror(mirror); }
void set_mirror(Axis axis, double mirror) { m_transformation.set_mirror(axis, mirror); }
void set_mirror(const Vec3d& mirror) { clear_cache(); m_transformation.set_mirror(mirror); }
void set_mirror(Axis axis, double mirror) { clear_cache(); m_transformation.set_mirror(axis, mirror); }
void convert_from_imperial_units();
void convert_from_meters();
@@ -1467,6 +1474,7 @@ struct GlobalSpeedMap
double topSolidInfillSpeed;
double supportSpeed;
double smallPerimeterSpeed;
double smallSupportPerimeterSpeed;
double maxSpeed;
Polygon bed_poly;
};
@@ -1588,8 +1596,8 @@ public:
ImportStepProgressFn stepFn,
StepIsUtf8Fn stepIsUtf8Fn,
std::function<int(Slic3r::Step&, double&, double&, bool&)> step_mesh_fn,
double linear_defletion,
double angle_defletion,
double linear_deflection,
double angle_deflection,
bool is_split_compound);
//BBS: add part plate related logic
+970
View File
@@ -0,0 +1,970 @@
#include "MultiNozzleUtils.hpp"
#include "Utils.hpp"
#include "ProjectTask.hpp" // Slic3r::FilamentInfo (StaticNozzleGroupResult / load_nozzle_infos_with_compatibility)
#include <algorithm>
#include <iomanip>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <boost/log/trivial.hpp>
// Multi-nozzle support.
namespace Slic3r { namespace MultiNozzleUtils {
// ==================== tool function implementations ====================
std::vector<NozzleInfo> build_nozzle_list(std::vector<NozzleGroupInfo> nozzle_groups)
{
std::vector<NozzleInfo> ret;
std::sort(nozzle_groups.begin(), nozzle_groups.end());
int nozzle_id = 0;
for (auto& group : nozzle_groups) {
for (int i = 0; i < group.nozzle_count; ++i) {
NozzleInfo tmp;
tmp.diameter = group.diameter;
tmp.extruder_id = group.extruder_id;
tmp.volume_type = group.volume_type;
tmp.group_id = nozzle_id++;
ret.emplace_back(std::move(tmp));
}
}
return ret;
}
std::vector<NozzleInfo> build_nozzle_list(double diameter, const std::vector<int>& filament_nozzle_map, const std::vector<int>& filament_volume_map, const std::vector<int>& filament_map)
{
std::string diameter_str = format_diameter_to_str(diameter);
std::map<int, std::vector<int>> nozzle_to_filaments;
for(size_t idx = 0; idx < filament_nozzle_map.size(); ++idx){
int nozzle_id = filament_nozzle_map[idx];
nozzle_to_filaments[nozzle_id].emplace_back(static_cast<int>(idx));
}
std::vector<NozzleInfo> ret;
for(auto& elem : nozzle_to_filaments){
int nozzle_id = elem.first;
auto& filaments = elem.second;
NozzleInfo info;
info.diameter = diameter_str;
info.group_id = nozzle_id;
info.extruder_id = filament_map[filaments.front()];
info.volume_type = NozzleVolumeType(filament_volume_map[filaments.front()]);
ret.emplace_back(std::move(info));
}
return ret;
}
void normalize_nozzle_map_per_layer(std::vector<std::vector<int>> &layer_filament_nozzle_maps,
const std::vector<std::vector<unsigned int>> &layer_filaments)
{
if (layer_filament_nozzle_maps.empty())
return;
const int total_layers = static_cast<int>(layer_filament_nozzle_maps.size());
int filament_count = 0;
for (const auto &layer_map : layer_filament_nozzle_maps)
filament_count = std::max(filament_count, static_cast<int>(layer_map.size()));
auto layer_uses_filament = [](const std::vector<unsigned int> &filaments, int filament_id) {
return std::find(filaments.begin(), filaments.end(), static_cast<unsigned int>(filament_id)) != filaments.end();
};
std::vector<int> last_used_nozzle(filament_count, -1);
std::unordered_map<int, int> first_used_nozzle;
std::unordered_map<int, int> first_used_layer;
// Forward pass: layers that extrude a filament define its nozzle; layers that don't inherit
// the nozzle it last used (carry-forward), remembering the first-ever nozzle for the back-fill.
for (int layer_id = 0; layer_id < total_layers; ++layer_id) {
auto &layer_map = layer_filament_nozzle_maps[layer_id];
const auto &used = layer_id < static_cast<int>(layer_filaments.size()) ? layer_filaments[layer_id] : std::vector<unsigned int>();
for (int filament_id = 0; filament_id < static_cast<int>(layer_map.size()); ++filament_id) {
if (layer_uses_filament(used, filament_id)) {
last_used_nozzle[filament_id] = layer_map[filament_id];
if (first_used_nozzle.count(filament_id) == 0) {
first_used_nozzle[filament_id] = layer_map[filament_id];
first_used_layer[filament_id] = layer_id;
}
} else if (last_used_nozzle[filament_id] >= 0) {
layer_map[filament_id] = last_used_nozzle[filament_id];
}
}
}
// Back-fill pass: layers before a filament's first use inherit the first nozzle it ever uses.
for (int layer_id = 0; layer_id < total_layers; ++layer_id) {
auto &layer_map = layer_filament_nozzle_maps[layer_id];
for (int filament_id = 0; filament_id < static_cast<int>(layer_map.size()); ++filament_id) {
if (first_used_layer.count(filament_id) != 0 && layer_id < first_used_layer[filament_id])
layer_map[filament_id] = first_used_nozzle[filament_id];
}
}
}
// ==================== LayeredNozzleGroupResult ====================
static bool has_filament_mapped_to_multiple_nozzles(const std::vector<std::vector<int>> &layer_filament_nozzle_maps,
const std::vector<unsigned int> &used_filaments)
{
if (layer_filament_nozzle_maps.empty() || used_filaments.empty())
return false;
for (auto filament_id_u : used_filaments) {
int filament_id = static_cast<int>(filament_id_u);
std::set<int> nozzle_ids;
for (size_t layer_id = 0; layer_id < layer_filament_nozzle_maps.size(); ++layer_id) {
const auto &map = layer_filament_nozzle_maps[layer_id];
if (filament_id < 0 || filament_id >= static_cast<int>(map.size()))
continue;
int nozzle_id = map[filament_id];
if (nozzle_id < 0)
continue;
nozzle_ids.insert(nozzle_id);
if (nozzle_ids.size() > 1)
return true;
}
}
return false;
}
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
const std::vector<int>& filament_nozzle_map,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments)
{
if (filament_nozzle_map.empty() || nozzle_list.empty()) {
return std::nullopt;
}
LayeredNozzleGroupResult result(false);
result._default_filament_nozzle_map = filament_nozzle_map;
result._nozzle_list = nozzle_list;
result._used_filaments = used_filaments;
return result;
}
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
const std::vector<std::vector<int>>& layer_filament_nozzle_maps,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::vector<unsigned int>>& layer_filament_sequences)
{
if (layer_filament_nozzle_maps.empty() || nozzle_list.empty()) {
return std::nullopt;
}
bool support_dynamic_nozzle_map = has_filament_mapped_to_multiple_nozzles(layer_filament_nozzle_maps, used_filaments);
LayeredNozzleGroupResult result(support_dynamic_nozzle_map);
result._layer_filament_nozzle_maps = layer_filament_nozzle_maps;
result._layer_filament_sequences = layer_filament_sequences;
result._nozzle_list = nozzle_list;
result._used_filaments = used_filaments;
if (!layer_filament_nozzle_maps.empty()) {
result._default_filament_nozzle_map = layer_filament_nozzle_maps[0];
}
return result;
}
std::optional<LayeredNozzleGroupResult> LayeredNozzleGroupResult::create(
const std::vector<unsigned int>& used_filaments,
const std::vector<int>& filament_map,
const std::vector<int>& filament_volume_map,
const std::vector<int>& filament_nozzle_map,
const std::vector<std::map<NozzleVolumeType, int>> &nozzle_count,
float diameter)
{
std::vector<NozzleGroupInfo> nozzle_groups;
for (size_t extruder_id = 0; extruder_id < nozzle_count.size(); ++extruder_id) {
for (auto elem : nozzle_count[extruder_id]) {
NozzleGroupInfo group_info;
group_info.diameter = format_diameter_to_str(diameter);
group_info.volume_type = elem.first;
group_info.nozzle_count = elem.second;
group_info.extruder_id = static_cast<int>(extruder_id);
nozzle_groups.emplace_back(group_info);
}
}
auto nozzle_list = build_nozzle_list(nozzle_groups);
std::vector<bool> used_nozzle(nozzle_list.size(), false);
std::map<int, int> input_nozzle_id_to_output;
std::vector<int> output_nozzle_map(filament_nozzle_map.size(), 0);
for (auto filament_idx : used_filaments) {
NozzleVolumeType req_type = NozzleVolumeType(filament_volume_map[filament_idx]);
int req_extruder = filament_map[filament_idx];
int input_nozzle_idx = filament_nozzle_map[filament_idx];
if (input_nozzle_id_to_output.find(input_nozzle_idx) != input_nozzle_id_to_output.end()) {
output_nozzle_map[filament_idx] = input_nozzle_id_to_output[input_nozzle_idx];
continue;
}
int output_nozzle_idx = -1;
for (size_t nozzle_idx = 0; nozzle_idx < nozzle_list.size(); ++nozzle_idx) {
if (used_nozzle[nozzle_idx]) continue;
auto &nozzle_info = nozzle_list[nozzle_idx];
if (!(nozzle_info.extruder_id == req_extruder && nozzle_info.volume_type == req_type)) continue;
output_nozzle_idx = static_cast<int>(nozzle_idx);
input_nozzle_id_to_output[input_nozzle_idx] = output_nozzle_idx;
used_nozzle[nozzle_idx] = true;
break;
}
if (output_nozzle_idx == -1) { return std::nullopt; }
output_nozzle_map[filament_idx] = output_nozzle_idx;
}
return create(output_nozzle_map, nozzle_list, used_filaments);
}
bool LayeredNozzleGroupResult::are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id) const
{
std::optional<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
std::optional<NozzleInfo> nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id);
if (!nozzle_info1 || !nozzle_info2) return false;
return nozzle_info1->extruder_id == nozzle_info2->extruder_id;
}
bool LayeredNozzleGroupResult::are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id) const
{
std::optional<NozzleInfo> nozzle_info1 = get_nozzle_for_filament(filament_id1, layer_id);
std::optional<NozzleInfo> nozzle_info2 = get_nozzle_for_filament(filament_id2, layer_id);
if (!nozzle_info1 || !nozzle_info2) return false;
return nozzle_info1->group_id == nozzle_info2->group_id;
}
int LayeredNozzleGroupResult::get_extruder_count() const
{
std::set<int> extruder_ids;
for (const auto &nozzle : _nozzle_list) { extruder_ids.insert(nozzle.extruder_id); }
return static_cast<int>(extruder_ids.size());
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
{
return get_used_nozzles_in_extruder(target_extruder_id, -1);
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const
{
std::set<int> nozzle_ids;
std::vector<NozzleInfo> result;
std::vector<unsigned int> target_filaments = get_used_filaments(layer_id);
for (unsigned int filament_id : target_filaments) {
if (layer_id != -1) {
auto nozzle_opt = get_nozzle_for_filament(static_cast<int>(filament_id), layer_id);
if (nozzle_opt) {
if (target_extruder_id == -1 || nozzle_opt->extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle_opt->group_id); }
}
} else {
auto nozzles = get_nozzles_for_filament(static_cast<int>(filament_id));
for (const auto &nozzle : nozzles) {
if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) { nozzle_ids.insert(nozzle.group_id); }
}
}
}
for (int nozzle_id : nozzle_ids) {
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[nozzle_id]); }
}
return result;
}
std::vector<int> LayeredNozzleGroupResult::get_used_extruders() const
{
return get_used_extruders(-1);
}
std::vector<int> LayeredNozzleGroupResult::get_used_extruders(int layer_id) const
{
std::set<int> used_extruders;
// used filaments on the given layer (or globally)
std::vector<unsigned int> target_filaments = get_used_filaments(layer_id);
for (auto filament_id : target_filaments) {
if (layer_id != -1) {
// single-layer: nozzle used by this filament on this layer
auto nozzle_opt = get_nozzle_for_filament(static_cast<int>(filament_id), layer_id);
if (nozzle_opt) { used_extruders.insert(nozzle_opt->extruder_id); }
} else {
// global: every nozzle this filament uses across all layers
auto nozzles = get_nozzles_for_filament(static_cast<int>(filament_id));
for (const auto &nozzle : nozzles) { used_extruders.insert(nozzle.extruder_id); }
}
}
return std::vector<int>(used_extruders.begin(), used_extruders.end());
}
std::vector<int> LayeredNozzleGroupResult::get_extruder_map(bool zero_based, int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> extruder_map(filament_nozzle_map.size());
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
int nozzle_id = filament_nozzle_map[idx];
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
extruder_map[idx] = _nozzle_list[nozzle_id].extruder_id;
} else {
extruder_map[idx] = -1;
}
}
if (zero_based) return extruder_map;
auto new_filament_map = extruder_map;
std::transform(new_filament_map.begin(), new_filament_map.end(), new_filament_map.begin(), [](int val) { return val + 1; });
return new_filament_map;
}
std::vector<int> LayeredNozzleGroupResult::get_nozzle_map(int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> nozzle_map(filament_nozzle_map.size());
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
int nozzle_id = filament_nozzle_map[idx];
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
nozzle_map[idx] = _nozzle_list[nozzle_id].group_id;
} else {
nozzle_map[idx] = -1;
}
}
return nozzle_map;
}
std::vector<int> LayeredNozzleGroupResult::get_volume_map(int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
std::vector<int> volume_map(filament_nozzle_map.size());
for (size_t idx = 0; idx < filament_nozzle_map.size(); ++idx) {
int nozzle_id = filament_nozzle_map[idx];
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(_nozzle_list.size())) {
volume_map[idx] = _nozzle_list[nozzle_id].volume_type;
} else {
volume_map[idx] = -1;
}
}
return volume_map;
}
std::vector<unsigned int> LayeredNozzleGroupResult::get_used_filaments(int layer_id) const
{
if (layer_id < 0) { return _used_filaments; }
if (layer_id >= static_cast<int>(_layer_filament_nozzle_maps.size())) { return _used_filaments; }
if (!_layer_filament_sequences.empty() && layer_id < static_cast<int>(_layer_filament_sequences.size())) {
return _layer_filament_sequences[layer_id];
}
return {};
}
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_for_filament(int filament_id, int layer_id) const
{
const std::vector<int> &filament_nozzle_map = get_layer_filament_nozzle_map(layer_id);
if (filament_id < 0 || filament_id >= static_cast<int>(filament_nozzle_map.size())) { return std::nullopt; }
int nozzle_id = filament_nozzle_map[filament_id];
return get_nozzle_from_id(nozzle_id);
}
std::vector<NozzleInfo> LayeredNozzleGroupResult::get_nozzles_for_filament(int filament_id) const
{
std::set<int> nozzle_ids;
if (!support_dynamic_nozzle_map) {
if (filament_id >= 0 && filament_id < static_cast<int>(_default_filament_nozzle_map.size())) {
nozzle_ids.insert(_default_filament_nozzle_map[filament_id]);
}
} else {
int start_layer = 0;
int end_layer = static_cast<int>(_layer_filament_nozzle_maps.size());
for (int i = start_layer; i < end_layer; ++i) {
const auto &map = _layer_filament_nozzle_maps[i];
if (filament_id >= 0 && filament_id < static_cast<int>(map.size())) {
nozzle_ids.insert(map[filament_id]);
}
}
}
std::vector<NozzleInfo> result;
for (int id : nozzle_ids) {
if (id >= 0 && id < static_cast<int>(_nozzle_list.size())) { result.push_back(_nozzle_list[id]); }
}
return result;
}
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const
{
if (filament_id < 0) return std::nullopt;
if (!support_dynamic_nozzle_map) {
if (filament_id >= static_cast<int>(_default_filament_nozzle_map.size())) return std::nullopt;
return get_nozzle_from_id(_default_filament_nozzle_map[filament_id]);
}
for (size_t layer = 0; layer < _layer_filament_nozzle_maps.size(); ++layer) {
auto layer_used_filaments = get_used_filaments(layer);
if (std::find(layer_used_filaments.begin(), layer_used_filaments.end(), static_cast<unsigned int>(filament_id)) == layer_used_filaments.end()){
continue;
}
const auto &map = _layer_filament_nozzle_maps[layer];
if (filament_id >= 0 && filament_id < static_cast<int>(map.size())) {
int nozzle_id = map[filament_id];
auto nozzle = get_nozzle_from_id(nozzle_id);
if (nozzle) return nozzle;
}
}
return std::nullopt;
}
std::optional<NozzleInfo> LayeredNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const
{
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(_nozzle_list.size())) { return std::nullopt; }
return _nozzle_list[nozzle_id];
}
int LayeredNozzleGroupResult::get_extruder_id(int filament_id, int layer_id) const
{
auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id);
return nozzle_info ? nozzle_info->extruder_id : -1;
}
int LayeredNozzleGroupResult::get_nozzle_id(int filament_id, int layer_id) const
{
auto nozzle_info = get_nozzle_for_filament(filament_id, layer_id);
return nozzle_info ? nozzle_info->group_id : -1;
}
const std::vector<int> &LayeredNozzleGroupResult::get_layer_filament_nozzle_map(int layer_id) const
{
if (layer_id >= 0 && layer_id < static_cast<int>(_layer_filament_nozzle_maps.size())) { return _layer_filament_nozzle_maps[layer_id]; }
return _default_filament_nozzle_map;
}
// ==================== filament-change-time model ====================
FilamentChangeSimResult simulate_filament_change_time(
const std::vector<int>& logical_filaments,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
const std::vector<int>& group_of_filament,
const FilamentChangeTimeParams& time_params,
const std::vector<bool>& ams_preload_enabled,
bool calc_sliced_time)
{
FilamentChangeSimResult result;
if (logical_filaments.empty() || nozzle_list.empty() || filament_change_seq.empty() || nozzle_change_seq.empty())
return result;
// Re-map the parameter semantics:
// standard = AMS -> selector -> extruder (full path), selector = selector -> extruder (short path)
// so AMS -> selector = standard - selector
const float load_ams_to_selector = time_params.standard_load_time - time_params.selector_load_time;
const float unload_ams_to_selector = time_params.standard_unload_time - time_params.selector_unload_time;
const float load_selector_to_ext = time_params.selector_load_time;
const float unload_ext_to_selector = time_params.selector_unload_time;
// nozzle_id -> extruder_id
std::unordered_map<int, int> nozzle_to_extruder;
nozzle_to_extruder.reserve(nozzle_list.size());
for (const auto& nozzle : nozzle_list)
nozzle_to_extruder[nozzle.group_id] = nozzle.extruder_id;
// filament_id -> AMS group
std::unordered_map<int, int> filament_to_group;
filament_to_group.reserve(logical_filaments.size());
for (size_t i = 0; i < logical_filaments.size(); ++i)
filament_to_group[logical_filaments[i]] = group_of_filament[i];
const auto get_group = [&](int filament_id) -> int {
auto it = filament_to_group.find(filament_id);
return it != filament_to_group.end() ? it->second : -1;
};
const auto is_preload_enabled = [&](int group_id) -> bool {
if (group_id < 0 || group_id >= static_cast<int>(ams_preload_enabled.size()))
return false;
return ams_preload_enabled[group_id];
};
// Filament location states
enum class Location { IN_AMS, IN_SELECTOR, IN_EXTRUDER };
std::unordered_map<int, Location> filament_location; // filament_id -> current location
std::unordered_map<int, int> filament_extruder; // filament_id -> extruder it sits in (only valid when IN_EXTRUDER)
std::unordered_map<int, int> extruder_filament; // extruder_id -> currently loaded filament
// group_id -> filaments currently occupying that AMS channel (IN_SELECTOR or IN_EXTRUDER)
std::unordered_map<int, std::unordered_set<int>> ams_group_occupied;
filament_location.reserve(logical_filaments.size());
filament_extruder.reserve(logical_filaments.size());
// Initial state: every filament is in the AMS, every extruder is empty
for (int f : logical_filaments)
filament_location[f] = Location::IN_AMS;
// Slicer-estimate simulator: use NozzleStatusRecorder to track what each nozzle/extruder holds during slicing
NozzleStatusRecorder sliced_recorder;
const size_t seq_len = std::min(filament_change_seq.size(), nozzle_change_seq.size());
double actual_time = 0.0;
double sliced_time = 0.0;
for (size_t i = 0; i < seq_len; ++i) {
int B = filament_change_seq[i];
int nozzle_id = nozzle_change_seq[i];
auto nozzle_iter = nozzle_to_extruder.find(nozzle_id);
if (nozzle_iter == nozzle_to_extruder.end()) continue;
int E = nozzle_iter->second; // target extruder
// Step 0: compute the slicer-estimated time
// Slicer estimate: simulate the slicer's view (no selector awareness);
// count a load/unload when nozzle_in_extruder_change || filament_in_nozzle_change
if (calc_sliced_time) {
int old_nozzle_in_E = sliced_recorder.get_nozzle_in_extruder(E);
int old_filament_in_nozzle = sliced_recorder.get_filament_in_nozzle(nozzle_id);
int old_filament_in_ext = sliced_recorder.get_filament_in_nozzle(old_nozzle_in_E);
bool nozzle_change = (old_nozzle_in_E != nozzle_id);
bool filament_change = (old_filament_in_nozzle != B);
if (nozzle_change || filament_change) {
if (old_filament_in_ext != -1)
sliced_time += time_params.standard_unload_time;
sliced_time += time_params.standard_load_time;
}
sliced_recorder.set_nozzle_status(nozzle_id, B, E);
}
// Step 1: find the filament A currently loaded in the target extruder E
int A = -1;
{
auto it = extruder_filament.find(E);
if (it != extruder_filament.end())
A = it->second;
}
int group_B = get_group(B);
int group_A = (A != -1) ? get_group(A) : -1;
// Step 2: clear B's AMS-channel occupancy
auto group_it = ams_group_occupied.find(group_B);
if (group_it != ams_group_occupied.end()) {
for (int X : group_it->second) {
if (X == B) continue;
// X shares B's AMS channel, retreat it to the AMS to make way
Location loc_X = filament_location[X];
if (loc_X == Location::IN_EXTRUDER) {
actual_time += unload_ext_to_selector + unload_ams_to_selector;
int E2 = filament_extruder[X];
extruder_filament.erase(E2);
filament_extruder.erase(X);
} else if (loc_X == Location::IN_SELECTOR) {
actual_time += unload_ams_to_selector;
}
filament_location[X] = Location::IN_AMS;
}
group_it->second.clear();
}
// Step 3: A exits E (while A is still in the extruder)
// Step 3.5: pre-load B (in parallel with Step 3)
// actual time = max(Step 3, Step 3.5)
bool step3_executed = false;
float step3_time = 0.0f;
if (A != -1 && A != B && filament_location[A] == Location::IN_EXTRUDER) {
if (is_preload_enabled(group_A) && group_A != group_B) {
step3_time = unload_ext_to_selector;
filament_location[A] = Location::IN_SELECTOR;
} else {
step3_time = unload_ext_to_selector + unload_ams_to_selector;
filament_location[A] = Location::IN_AMS;
ams_group_occupied[group_A].erase(A);
}
extruder_filament.erase(E);
filament_extruder.erase(A);
step3_executed = true;
}
float step3_5_time = 0.0f;
if (step3_executed &&
filament_location[B] == Location::IN_AMS &&
group_A != group_B &&
is_preload_enabled(group_B)) {
step3_5_time = load_ams_to_selector;
filament_location[B] = Location::IN_SELECTOR;
ams_group_occupied[group_B].insert(B);
}
actual_time += std::max(step3_time, step3_5_time);
// Step 4: push B into E
// Step 6: pre-load the next filament C (in parallel with Step 4)
// actual time = max(Step 4, Step 6)
float step4_time = 0.0f;
Location loc_B = filament_location[B];
if (loc_B == Location::IN_AMS) {
step4_time = load_ams_to_selector + load_selector_to_ext;
} else if (loc_B == Location::IN_SELECTOR) {
step4_time = load_selector_to_ext;
}
// Step 5: update state
extruder_filament[E] = B;
filament_location[B] = Location::IN_EXTRUDER;
filament_extruder[B] = E;
ams_group_occupied[group_B].insert(B);
float step6_time = 0.0f;
if (i + 1 < seq_len) {
int C = filament_change_seq[i + 1];
int group_C = get_group(C);
if (filament_location[C] == Location::IN_AMS &&
group_C != group_B &&
is_preload_enabled(group_C) &&
ams_group_occupied[group_C].empty()) {
step6_time = load_ams_to_selector;
filament_location[C] = Location::IN_SELECTOR;
ams_group_occupied[group_C].insert(C);
}
}
actual_time += std::max(step4_time, step6_time);
}
result.actual_time = actual_time;
result.sliced_time = sliced_time;
return result;
}
// ==================== NozzleStatusRecorder implementation ====================
bool NozzleStatusRecorder::is_nozzle_empty(int nozzle_id) const
{
auto iter = nozzle_filament_status.find(nozzle_id);
if (iter == nozzle_filament_status.end()) return true;
return false;
}
int NozzleStatusRecorder::get_filament_in_nozzle(int nozzle_id) const
{
auto iter = nozzle_filament_status.find(nozzle_id);
if (iter == nozzle_filament_status.end()) return -1;
return iter->second;
}
int NozzleStatusRecorder::get_nozzle_in_extruder(int extruder_id) const
{
auto iter = extruder_nozzle_status.find(extruder_id);
if (iter == extruder_nozzle_status.end()) return -1;
return iter->second;
}
void NozzleStatusRecorder::set_nozzle_status(int nozzle_id, int filament_id, int extruder_id)
{
nozzle_filament_status[nozzle_id] = filament_id;
if (extruder_id != -1) {
extruder_nozzle_status[extruder_id] = nozzle_id;
}
}
void NozzleStatusRecorder::clear_nozzle_status(int nozzle_id)
{
auto iter = nozzle_filament_status.find(nozzle_id);
if (iter == nozzle_filament_status.end()) return;
nozzle_filament_status.erase(iter);
}
int LayeredNozzleGroupResult::estimate_seq_flush_weight(const std::vector<std::vector<std::vector<float>>>& flush_matrix, const std::vector<int>& filament_change_seq) const
{
auto get_weight_from_volume = [](float volume){
return static_cast<int>(volume * 1.26 * 0.01);
};
float total_flush_volume = 0;
NozzleStatusRecorder recorder;
for(auto filament: filament_change_seq){
auto nozzle = get_nozzle_for_filament(filament, -1);
if(!nozzle)
continue;
int extruder_id = nozzle->extruder_id;
int nozzle_id = nozzle->group_id;
int last_filament = recorder.get_filament_in_nozzle(nozzle_id);
if(last_filament!= -1 && last_filament != filament){
// bounds check to avoid out-of-range access
if (extruder_id >= 0 && extruder_id < static_cast<int>(flush_matrix.size()) &&
last_filament >= 0 && last_filament < static_cast<int>(flush_matrix[extruder_id].size()) &&
filament >= 0 && filament < static_cast<int>(flush_matrix[extruder_id][last_filament].size())) {
float flush_volume = flush_matrix[extruder_id][last_filament][filament];
total_flush_volume += flush_volume;
}
}
recorder.set_nozzle_status(nozzle_id, filament);
}
return get_weight_from_volume(total_flush_volume);
}
// ==================== StaticNozzleGroupResult ====================
std::optional<StaticNozzleGroupResult> StaticNozzleGroupResult::create(
const std::vector<FilamentInfo>& filaments_info,
const std::vector<NozzleInfo>& nozzles_info,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
bool support_dynamic_nozzle_map)
{
if (filaments_info.empty() || nozzles_info.empty()) return std::nullopt;
std::map<int, NozzleInfo> nozzle_list_map;
std::map<int, std::set<int>> filament_to_nozzles;
for (auto nozzle_info : nozzles_info)
nozzle_list_map[nozzle_info.group_id] = nozzle_info;
for (auto filament_info : filaments_info) {
auto fil_id = filament_info.id;
auto nozzles_id = filament_info.group_id;
std::set<int> nozzles_set(nozzles_id.begin(), nozzles_id.end());
// Backward compat with older (single-nozzle) gcode.3mf: filament has no group_id, avoid an empty map.
if (nozzles_set.empty()) {
for (const auto& nozzle_entry : nozzle_list_map)
nozzles_set.insert(nozzle_entry.first);
}
filament_to_nozzles[fil_id] = nozzles_set;
}
StaticNozzleGroupResult result(support_dynamic_nozzle_map);
result._filament_to_nozzles = filament_to_nozzles;
result._nozzle_list_map = nozzle_list_map;
result._filament_change_seq = filament_change_seq;
result._nozzle_change_seq = nozzle_change_seq;
return result;
}
std::optional<NozzleInfo> StaticNozzleGroupResult::get_nozzle_from_id(int nozzle_id) const
{
auto iter = _nozzle_list_map.find(nozzle_id);
if (iter == _nozzle_list_map.end()) { return std::nullopt; }
return iter->second;
}
int StaticNozzleGroupResult::get_extruder_count() const
{
std::set<int> extruder_ids;
for (const auto &elem : _nozzle_list_map) { extruder_ids.insert(elem.second.extruder_id); }
return static_cast<int>(extruder_ids.size());
}
std::vector<NozzleInfo> StaticNozzleGroupResult::get_used_nozzles_in_extruder(int target_extruder_id) const
{
std::vector<NozzleInfo> result;
for (const auto &elem : _nozzle_list_map) {
const auto &nozzle = elem.second;
if (target_extruder_id == -1 || nozzle.extruder_id == target_extruder_id) {
result.push_back(nozzle);
}
}
return result;
}
std::vector<int> StaticNozzleGroupResult::get_used_extruders() const
{
std::set<int> used_extruders;
for (const auto &elem : _nozzle_list_map) { used_extruders.insert(elem.second.extruder_id); }
return std::vector<int>(used_extruders.begin(), used_extruders.end());
}
std::vector<unsigned int> StaticNozzleGroupResult::get_used_filaments() const
{
std::vector<unsigned int> used_filaments;
used_filaments.reserve(_filament_to_nozzles.size());
for (const auto &elem : _filament_to_nozzles) {
if (elem.first >= 0) {
used_filaments.push_back(static_cast<unsigned int>(elem.first));
}
}
return used_filaments;
}
std::vector<NozzleInfo> StaticNozzleGroupResult::get_nozzles_for_filament(int filament_id) const
{
auto iter = _filament_to_nozzles.find(filament_id);
if (iter == _filament_to_nozzles.end()) { return std::vector<NozzleInfo>(); }
std::vector<NozzleInfo> result;
for (int nozzle_id : iter->second) {
auto nozzle_iter = _nozzle_list_map.find(nozzle_id);
if (nozzle_iter != _nozzle_list_map.end()) {
result.push_back(nozzle_iter->second);
}
}
return result;
}
std::optional<NozzleInfo> StaticNozzleGroupResult::get_first_nozzle_for_filament(int filament_id) const
{
if (filament_id < 0) return std::nullopt;
if (!_filament_change_seq.empty() && _filament_change_seq.size() == _nozzle_change_seq.size()) {
for (size_t idx = 0; idx < _filament_change_seq.size(); ++idx) {
if (_filament_change_seq[idx] == filament_id) {
int nozzle_id = _nozzle_change_seq[idx];
auto nozzle = get_nozzle_from_id(nozzle_id);
if (nozzle) return nozzle;
}
}
}
auto iter = _filament_to_nozzles.find(filament_id);
if (iter == _filament_to_nozzles.end()) return std::nullopt;
for (int nozzle_id : iter->second) {
auto nozzle = get_nozzle_from_id(nozzle_id);
if (nozzle) return nozzle;
}
return std::nullopt;
}
// ==================== serialization ====================
std::string NozzleInfo::serialize() const
{
std::ostringstream oss;
oss << "id=\"" << group_id << "\" "
<< "extruder_id=\"" << extruder_id + 1 << "\" "
<< "nozzle_diameter=\"" << diameter << "\" "
<< "volume_type=\"" << get_nozzle_volume_type_string(volume_type) << "\"";
return oss.str();
}
std::string NozzleGroupInfo::serialize() const
{
std::ostringstream oss;
oss << extruder_id << "-"
<< std::setprecision(2) << diameter << "-"
<< get_nozzle_volume_type_string(volume_type) << "-"
<< nozzle_count;
return oss.str();
}
std::optional<NozzleGroupInfo> NozzleGroupInfo::deserialize(const std::string &str)
{
std::istringstream iss(str);
std::string token;
std::vector<std::string> tokens;
while (std::getline(iss, token, '-')) { tokens.push_back(token); }
if (tokens.size() != 4) { return std::nullopt; }
try {
int extruder_id = std::stoi(tokens[0]);
std::string diameter = tokens[1];
NozzleVolumeType volume_type = NozzleVolumeType(ConfigOptionEnum<NozzleVolumeType>::get_enum_values().at(tokens[2]));
int nozzle_count = std::stoi(tokens[3]);
return NozzleGroupInfo(diameter, volume_type, extruder_id, nozzle_count);
} catch (const std::exception &) {
return std::nullopt;
}
}
std::vector<NozzleInfo> load_nozzle_infos_with_compatibility(
const std::vector<NozzleInfo>& nozzle_infos,
const std::vector<FilamentInfo>& filament_infos,
const std::vector<int>& filament_map,
const std::vector<NozzleVolumeType>& extruder_volume_types,
const std::vector<double>& nozzle_diameter
)
{
bool has_nozzle_info = !nozzle_infos.empty();
bool has_valid_filament_info = !filament_infos.empty() && std::all_of(filament_infos.begin(), filament_infos.end(), [](const FilamentInfo& info){
return info.group_id.size() == 1;
});
if(!has_nozzle_info && !has_valid_filament_info){
BOOST_LOG_TRIVIAL(warning)<<__FUNCTION__ << ": building nozzle list from filament map and volume types";
// Backward compatibility for older gcode.3mf:
// - nozzle_diameter is always present and its size defines extruder count.
// - filament_map may be missing; treat it as [0, 0, ...] for each extruder.
// - extruder_volume_types may be missing; treat it as all Standard.
const size_t extruder_count = nozzle_diameter.size();
std::vector<NozzleVolumeType> volume_types_fixed = extruder_volume_types;
volume_types_fixed.resize(extruder_count, NozzleVolumeType::nvtStandard);
std::vector<NozzleInfo> result;
result.reserve(extruder_count);
for (size_t extruder_id = 0; extruder_id < extruder_count; ++extruder_id) {
NozzleInfo info;
info.diameter = format_diameter_to_str(nozzle_diameter[extruder_id]);
info.group_id = static_cast<int>(extruder_id);
info.extruder_id = static_cast<int>(extruder_id);
info.volume_type = volume_types_fixed[extruder_id];
result.emplace_back(std::move(info));
}
return result;
}
if(!has_nozzle_info){
BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": building nozzle list from filament info";
std::map<int, NozzleInfo> nozzle_map; // group_id -> NozzleInfo
for(auto& filament : filament_infos){
int group_id = filament.group_id.front();
if(group_id < 0 || nozzle_map.find(group_id) != nozzle_map.end()){
continue;
}
auto volume_type_str_to_enum = ConfigOptionEnum<NozzleVolumeType>::get_enum_values();
NozzleInfo info;
info.diameter = format_diameter_to_str(filament.nozzle_diameter);
info.group_id = group_id;
// Orca: bounds-check filament_map[filament.id] so a malformed 3mf (filament id
// beyond the map) degrades to extruder 0 instead of dereferencing out of range.
info.extruder_id = (filament.id >= 0 && filament.id < static_cast<int>(filament_map.size()))
? filament_map[filament.id] - 1
: 0; // to 0-based
if (volume_type_str_to_enum.count(filament.nozzle_volume_type))
info.volume_type = NozzleVolumeType(volume_type_str_to_enum.at(filament.nozzle_volume_type));
else {
info.volume_type = NozzleVolumeType::nvtStandard;
}
nozzle_map[group_id] = std::move(info);
}
std::vector<NozzleInfo> ret;
for(auto& elem : nozzle_map){
ret.emplace_back(elem.second);
}
return ret;
}
auto result = nozzle_infos;
std::sort(result.begin(), result.end());
BOOST_LOG_TRIVIAL(info)<<__FUNCTION__ << ": using new 3mf format with " << result.size() << " nozzle infos.";
return result;
}
}} // namespace Slic3r::MultiNozzleUtils
+296
View File
@@ -0,0 +1,296 @@
#ifndef MULTI_NOZZLE_UTILS_HPP
#define MULTI_NOZZLE_UTILS_HPP
#include <vector>
#include <map>
#include <optional>
#include <set>
#include <unordered_map>
#include "PrintConfig.hpp"
// Multi-nozzle support types.
// Declares the filament-grouping result types the slicing pipeline needs, plus the analytic
// filament-change-time model (FilamentChangeTimeParams, NozzleStatusRecorder,
// FilamentChangeSimResult, simulate_filament_change_time) — self-contained analytic code that
// never touches the time estimator; its first consumer is the filament_group golden harness.
// The gcode.3mf serialization surface lives here too: NozzleInfo/NozzleGroupInfo
// serialize+deserialize, the device-side StaticNozzleGroupResult,
// load_nozzle_infos_with_compatibility (the backward-compat 3mf reader) and
// LayeredNozzleGroupResult::estimate_seq_flush_weight. The change-time-tuning helpers
// calc_filament_change_gap_for_assignment / find_optimal_physical_assignment (used only by the
// AMS pre-load optimizer, a later feature) are not implemented here.
namespace Slic3r {
struct FilamentInfo; // Slic3r::FilamentInfo (ProjectTask.hpp) — consumed by StaticNozzleGroupResult / the 3mf reader
namespace MultiNozzleUtils {
// Information about a single logical nozzle.
struct NozzleInfo
{
std::string diameter;
NozzleVolumeType volume_type;
int extruder_id{-1}; // logical extruder id
int group_id{-1}; // logical nozzle id
std::string serialize() const;
bool operator<(const NozzleInfo& other) const {
if(group_id != other.group_id) return group_id < other.group_id;
if(extruder_id != other.extruder_id) return extruder_id < other.extruder_id;
if(volume_type != other.volume_type) return volume_type < other.volume_type;
return diameter < other.diameter;
}
};
// A group of identical nozzles on one extruder (diameter + volume type + count).
struct NozzleGroupInfo
{
std::string diameter;
NozzleVolumeType volume_type;
int extruder_id;
int nozzle_count;
NozzleGroupInfo() = default;
NozzleGroupInfo(const std::string& nozzle_diameter_, const NozzleVolumeType volume_type_, const int extruder_id_, const int nozzle_count_)
: diameter(nozzle_diameter_), volume_type(volume_type_), extruder_id(extruder_id_), nozzle_count(nozzle_count_)
{}
inline bool operator<(const NozzleGroupInfo &rhs) const
{
if (extruder_id != rhs.extruder_id) return extruder_id < rhs.extruder_id;
if (diameter != rhs.diameter) return diameter < rhs.diameter;
if (volume_type != rhs.volume_type) return volume_type < rhs.volume_type;
return nozzle_count < rhs.nozzle_count;
}
bool is_same_type(const NozzleGroupInfo &rhs) const
{
return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id;
}
inline bool operator==(const NozzleGroupInfo &rhs) const
{
return diameter == rhs.diameter && volume_type == rhs.volume_type && extruder_id == rhs.extruder_id && nozzle_count == rhs.nozzle_count;
}
std::string serialize() const;
static std::optional<NozzleGroupInfo> deserialize(const std::string& str);
};
// Load/unload time constants used by the filament-change-time model.
// Consumed by simulate_filament_change_time() below and carried by the grouping-context
// substrate (FilamentGroupContext::SpeedInfo).
struct FilamentChangeTimeParams
{
float selector_load_time{0.0f};
float selector_unload_time{0.0f};
float standard_load_time{0.0f};
float standard_unload_time{0.0f};
};
/**
* @brief Abstract base for a nozzle-grouping result.
*/
class NozzleGroupResultBase
{
protected:
bool support_dynamic_nozzle_map{false}; // whether dynamic (selector) mapping is used
public:
NozzleGroupResultBase(bool support_dynamic_map = false) : support_dynamic_nozzle_map(support_dynamic_map) {}
virtual ~NozzleGroupResultBase() = default;
virtual std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const = 0;
virtual std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const = 0; // logical nozzle a filament first uses
virtual std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const = 0; // every nozzle a filament may use (across all layers)
bool is_support_dynamic_nozzle_map() const { return support_dynamic_nozzle_map; }
virtual int get_extruder_count() const = 0;
virtual std::vector<NozzleInfo> get_used_nozzles_in_extruder(int extruder_id =-1) const = 0;
virtual std::vector<int> get_used_extruders() const = 0;
virtual std::vector<unsigned int> get_used_filaments() const = 0;
};
/**
* @brief Layer-aware nozzle-grouping result.
* Used by the back-end slicing code; supports per-layer nozzle mapping.
*/
class LayeredNozzleGroupResult : public NozzleGroupResultBase
{
private:
std::vector<std::vector<int>> _layer_filament_nozzle_maps; // per-layer filament -> nozzle map
std::vector<std::vector<unsigned int>> _layer_filament_sequences; // per-layer filament print order
std::vector<int> _default_filament_nozzle_map; // global filament -> nozzle map
std::vector<unsigned int> _used_filaments; // all used filament indices
std::vector<NozzleInfo> _nozzle_list; // global nozzle list
public:
LayeredNozzleGroupResult(bool support_dynamic_map = false) : NozzleGroupResultBase(support_dynamic_map) {}
// No selector: one global filament->nozzle map.
static std::optional<LayeredNozzleGroupResult> create(
const std::vector<int>& filament_nozzle_map,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments);
// Selector: built from per-layer maps (each layer may differ).
static std::optional<LayeredNozzleGroupResult> create(
const std::vector<std::vector<int>>& layer_filament_nozzle_maps,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<unsigned int>& used_filaments,
const std::vector<std::vector<unsigned int>>& layer_filament_sequences);
// Multi-nozzle without selector: resolve each requested logical nozzle to a physical nozzle.
static std::optional<LayeredNozzleGroupResult> create(
const std::vector<unsigned int>& used_filaments,
const std::vector<int>& filament_map,
const std::vector<int>& filament_volume_map,
const std::vector<int>& filament_nozzle_map,
const std::vector<std::map<NozzleVolumeType, int>>& nozzle_count,
float diameter);
bool are_filaments_same_extruder(int filament_id1, int filament_id2, int layer_id = -1) const;
bool are_filaments_same_nozzle(int filament_id1, int filament_id2, int layer_id = -1) const;
int get_extruder_count() const override;
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int target_extruder_id = -1) const override;
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int target_extruder_id, int layer_id) const; // layer_id=-1 uses default map
std::vector<int> get_used_extruders() const override;
std::vector<int> get_used_extruders(int layer_id) const; // layer_id=-1 returns global extruders
std::vector<int> get_extruder_map(bool zero_based = true, int layer_id = -1) const;
std::vector<int> get_nozzle_map(int layer_id = -1) const;
std::vector<int> get_volume_map(int layer_id = -1) const;
std::vector<unsigned int> get_used_filaments() const override { return _used_filaments; }
std::vector<unsigned int> get_used_filaments(int layer_id) const;
std::optional<NozzleInfo> get_nozzle_for_filament(int filament_id, int layer_id = -1) const;
std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const override;
std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const override;
std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const override;
int get_extruder_id(int filament_id, int layer_id = -1) const;
int get_nozzle_id(int filament_id, int layer_id = -1) const;
size_t get_layer_count() const { return _layer_filament_nozzle_maps.size(); }
const std::vector<int>& get_layer_filament_nozzle_map(int layer_id) const;
const std::vector<std::vector<int>> &get_layer_filament_nozzle_maps() const { return _layer_filament_nozzle_maps; }
const std::vector<std::vector<unsigned int>>& get_layer_filament_sequences() const { return _layer_filament_sequences; }
// Estimate the flush weight of a filament-change sequence given the per-extruder flush matrix
// (extruder -> from-filament -> to-filament).
int estimate_seq_flush_weight(const std::vector<std::vector<std::vector<float>>>& flush_matrix, const std::vector<int>& filament_change_seq) const;
};
/**
* @brief Layer-less nozzle-grouping result for the device side (static nozzle mapping only).
* Reconstructed from a loaded gcode.3mf together with the filament/nozzle change sequences.
*/
class StaticNozzleGroupResult : public NozzleGroupResultBase
{
private:
std::map<int, std::set<int>> _filament_to_nozzles; // every nozzle a filament may map to
std::map<int, NozzleInfo> _nozzle_list_map; // used nozzles, keyed by logical nozzle id
std::vector<int> _filament_change_seq; // filament sequence used to resolve first-use
std::vector<int> _nozzle_change_seq; // logical-nozzle sequence paired with the filament sequence
public:
StaticNozzleGroupResult(bool support_dynamic_map) : NozzleGroupResultBase(support_dynamic_map) {}
// Build from a loaded 3mf, with the filament/nozzle change sequences.
static std::optional<StaticNozzleGroupResult> create(
const std::vector<FilamentInfo>& filaments_info,
const std::vector<NozzleInfo>& nozzles_info,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
bool support_dynamic_map);
int get_extruder_count() const override;
std::vector<NozzleInfo> get_used_nozzles_in_extruder(int extruder_id = -1) const override;
std::vector<int> get_used_extruders() const override;
std::vector<unsigned int> get_used_filaments() const override;
std::optional<NozzleInfo> get_nozzle_from_id(int nozzle_id) const override;
std::vector<NozzleInfo> get_nozzles_for_filament(int filament_id) const override;
std::optional<NozzleInfo> get_first_nozzle_for_filament(int filament_id) const override;
};
// Tracks, during the filament-change simulation, which filament sits in each physical nozzle
// and which nozzle each extruder currently carries.
class NozzleStatusRecorder
{
private:
std::unordered_map<int, int> nozzle_filament_status; // Track filament in each nozzle
std::unordered_map<int, int> extruder_nozzle_status; // Track nozzle for each extruder
int current_extruder_id_ = -1; // Track current extruder id
public:
NozzleStatusRecorder() = default;
bool is_nozzle_empty(int nozzle_id) const;
int get_filament_in_nozzle(int nozzle_id) const;
int get_nozzle_in_extruder(int extruder_id) const;
int get_current_extruder_id() const { return current_extruder_id_; }
void clear_nozzle_status(int nozzle_id);
void set_current_extruder_id(int extruder_id) { current_extruder_id_ = extruder_id; }
// Update the status of a nozzle with new filament and extruder information
void set_nozzle_status(int nozzle_id, int filament_id, int extruder_id = -1);
// key: nozzle id, value: filament id (-1 = the nozzle carries no filament)
const std::unordered_map<int, int>& get_nozzle_filament_map() const { return nozzle_filament_status; }
// key: extruder id, value: nozzle id (-1 = the extruder carries no nozzle)
const std::unordered_map<int, int>& get_extruder_nozzle_map() const { return extruder_nozzle_status; }
};
struct FilamentChangeSimResult {
double actual_time = 0.0;
double sliced_time = 0.0;
};
// Analytic filament-change-time model. Given the used filaments, the nozzle
// list, the filament/nozzle change sequences, each filament's AMS group and the load/unload time
// constants, it simulates AMS->selector->extruder transport (with optional AMS pre-load overlap)
// and returns the actual print time plus the slicer-estimated time. Self-contained: it never
// touches the g-code time estimator.
FilamentChangeSimResult simulate_filament_change_time(
const std::vector<int>& logical_filaments,
const std::vector<NozzleInfo>& nozzle_list,
const std::vector<int>& filament_change_seq,
const std::vector<int>& nozzle_change_seq,
const std::vector<int>& group_of_filament,
const FilamentChangeTimeParams& time_params,
const std::vector<bool>& ams_preload_enabled = {},
bool calc_sliced_time = false);
// ==================== tool functions ====================
// Make each filament's per-layer nozzle assignment gap-free: layers where a filament is not
// extruded inherit the nozzle it last used (forward carry); layers before its first use inherit
// the first nozzle it ever uses (back-fill). Entries on layers where the filament is actually
// used stay untouched. Needed for stitched sequential maps, where consumers indexing with an
// object-local layer id must resolve the same nozzle as global-id consumers except across a
// genuine mid-print reassignment.
void normalize_nozzle_map_per_layer(std::vector<std::vector<int>>& layer_filament_nozzle_maps,
const std::vector<std::vector<unsigned int>>& layer_filaments);
std::vector<NozzleInfo> build_nozzle_list(std::vector<NozzleGroupInfo> info);
std::vector<NozzleInfo> build_nozzle_list(double diameter, const std::vector<int>& filament_nozzle_map,
const std::vector<int>& filament_volume_map, const std::vector<int>& filament_map);
// Load nozzle infos from a gcode.3mf, handling backward compatibility with older 3mf that did not
// record standalone <nozzle> tags: falls back to the per-filament group_id/diameter/volume_type, and
// (for the oldest single-nozzle 3mf) to the filament_map + extruder volume types + nozzle diameters.
std::vector<NozzleInfo> load_nozzle_infos_with_compatibility(
const std::vector<NozzleInfo>& nozzle_infos,
const std::vector<FilamentInfo>& filament_infos,
const std::vector<int>& filament_map,
const std::vector<NozzleVolumeType>& extruder_volume_types,
const std::vector<double>& nozzle_diameter
);
} // namespace MultiNozzleUtils
} // namespace Slic3r
#endif // MULTI_NOZZLE_UTILS_HPP
+192 -15
View File
@@ -27,19 +27,20 @@ namespace Slic3r {
namespace orientation {
struct CostItems {
float overhang;
float bottom;
float bottom_hull;
float contour;
float area_laf; // area_of_low_angle_faces
float area_projected; // area of projected 2D profile
float volume;
float area_total; // total area of all faces
float radius; // radius of bounding box
float height_to_bottom_hull_ratio; // affects stability, the lower the better
float unprintability;
float overhang = 0;
float bottom = 0;
float bottom_hull = 0;
float contour = 0;
float area_laf = 0; // area_of_low_angle_faces
float area_projected = 0; // area of projected 2D profile
float volume = 0;
float area_total = 0; // total area of all faces
float radius = 0; // radius of bounding box
float height_to_bottom_hull_ratio = 0; // affects stability, the lower the better
float unprintability = 0;
Eigen::VectorXf areas_cooling;
CostItems(CostItems const & other) = default;
CostItems() { memset(this, 0, sizeof(*this)); }
CostItems() = default;
static std::string field_names() {
return " overhang, bottom, bothull, contour, A_laf, A_prj, unprintability";
}
@@ -68,10 +69,11 @@ public:
Eigen::VectorXf z_max, z_max_hull; // max of projected z
Eigen::VectorXf z_median; // median of projected z
Eigen::VectorXf z_mean; // mean of projected z
Eigen::VectorXf areas_cooling; // weighted areas for cool direction
std::vector<Vec3f> face_normals;
std::vector<Vec3f> face_normals_hull;
OrientParams params;
bool has_cooling_fan = false;
std::vector< Vec3f> orientations; // Vec3f == stl_normal
std::function<void(unsigned)> progressind = { }; // default empty indicator function
@@ -85,6 +87,7 @@ public:
orient_mesh = orient_mesh_;
mesh = &orient_mesh->mesh;
params = params_;
has_cooling_fan = orient_mesh->has_cooling_fan;
progressind = progressind_;
params.ASCENT = cos(PI - orient_mesh->overhang_angle * PI / 180); // use per-object overhang angle
@@ -158,12 +161,14 @@ public:
//To avoid flipping, we need to verify if there are orientations with same unprintability.
Vec3f n1 = {0, 0, 1};
auto best_orientation = results_vector[0].first;
size_t best_index = 0;
for (int i = 1; i< results_vector.size()-1; i++) {
if (abs(results_vector[i].second.unprintability - results_vector[0].second.unprintability) < EPSILON && abs(results_vector[0].first.dot(n1)-1) > EPSILON) {
if (abs(results_vector[i].first.dot(n1)-1) < EPSILON*EPSILON) {
if (abs(results_vector[i].first.dot(n1)-1) < EPSILON*EPSILON) {
best_orientation = n1;
break;
best_index = i;
break;
}
}
else {
@@ -172,6 +177,9 @@ public:
}
// cooling weights are per-orientation, so take them from the orientation actually chosen
areas_cooling = results_vector[best_index].second.areas_cooling;
BOOST_LOG_TRIVIAL(info) << std::fixed << std::setprecision(6) << "best:" << best_orientation.transpose() << ", costs:" << results_vector[0].second.field_values();
std::cout << std::fixed << std::setprecision(6) << "best:" << best_orientation.transpose() << ", costs:" << results_vector[0].second.field_values() << std::endl;
@@ -441,6 +449,19 @@ public:
Eigen::MatrixXf laf_areas = ((normal_projection_abs.array() < params.LAF_MAX) * (normal_projection_abs.array() > params.LAF_MIN) * (z_max.array() > total_min_z + params.FIRST_LAY_H)).select(areas, 0);
costs.area_laf = laf_areas.sum();
if (has_cooling_fan)
{
// Angle range of overhang faces requiring cooling
float angle_thres_high = -0.6427f;
float angle_thres_low = -0.97f;
// compute the weighted overhang faces area
Eigen::VectorXf ones_f = Eigen::VectorXf::Ones(mesh->facets_count());
auto overhang_area_condition = (normal_projection.array() < angle_thres_high && normal_projection.array() > angle_thres_low).eval();
Eigen::VectorXf areas_ = (overhang_area_condition * !bottom_condition_2nd).select(areas, 0);
Eigen::VectorXf weighted_areas = areas_.cwiseProduct(ones_f - normal_projection);
costs.areas_cooling = weighted_areas;
}
// height to bottom_hull_area ratio
//float total_max_z = z_projected.maxCoeff();
//costs.height_to_bottom_hull_ratio = SQ(total_max_z) / (costs.bottom_hull + 1e-7);
@@ -468,6 +489,67 @@ public:
return cost;
}
Vec3d find_cooling_direction2(Vec3d euler_angles, const Eigen::VectorXf& areas_in, TriangleMesh& mesh)
{
Vec3f machine_cool_dir = this->orient_mesh->cooling_direction.cast<float>();
const size_t num_faces = areas.rows();
Vec3f best_direction = { 0, 0, 0 };
// 1. Make a copy of input mesh, rotate and translate to the best orientation
TriangleMesh mesh_copy = TriangleMesh(mesh.its);
mesh_copy.rotate_x(euler_angles(0, 0));
mesh_copy.rotate_y(euler_angles(1, 0));
mesh_copy.rotate_z(euler_angles(2, 0));
auto bounding_box = mesh_copy.bounding_box();
Eigen::VectorXf translate_distance = bounding_box.min.array().cast<float>();
Vec3d mesh_center = mesh_copy.center();
mesh_copy.translate(-mesh_center(0), -mesh_center(1), -translate_distance(2));
// 2. sample cooling direction
const size_t sample_nums = 180;
std::vector<Vec3f> cool_dirs;
for (size_t i = 0; i < sample_nums; i++)
{
float angle_deg = i * (360.0 / sample_nums);
float angle_rad = angle_deg * (PI / 180.0);
cool_dirs.push_back(Vec3f{ std::cos(angle_rad), std::sin(angle_rad), 0});
}
// 3. accumulate the weighted projected overhang area, find the max weighted project area direction
std::vector<Vec3f> face_normals_copy = its_face_normals(mesh_copy.its);
float overhang_projected_max = 0.f;
float overhang_projected_origin = 0.f;
for (auto cool_dir : cool_dirs)
{
float overhang_projected_tmp = 0.f;
for (size_t i = 0; i < num_faces; i++)
{
float cool_dir_projection = face_normals_copy[i].dot(cool_dir);
if (areas_in[i] > 0 && cool_dir_projection > 0)
{
overhang_projected_tmp += areas_in[i] * cool_dir_projection;
}
}
if (overhang_projected_tmp > overhang_projected_max)
{
overhang_projected_max = overhang_projected_tmp;
best_direction = cool_dir;
}
if (cool_dir.dot(machine_cool_dir) > 0.999)
{
overhang_projected_origin = overhang_projected_tmp;
}
}
// The symmetric model has similar overhang projection at all angles, so Z-axis rotation is unnecessary.
if (std::abs(overhang_projected_origin - overhang_projected_max) < 1.0f)
{
best_direction = machine_cool_dir;
}
BOOST_LOG_TRIVIAL(info) << "best cooling dir = " << best_direction.transpose() << "\n";
return best_direction.cast<double>();
}
};
void _orient(OrientMeshs& meshs_,
@@ -497,6 +579,13 @@ void _orient(OrientMeshs& meshs_,
mesh_.orientation = orienter.process();
Geometry::rotation_from_two_vectors(mesh_.orientation, { 0,0,1 }, mesh_.axis, mesh_.angle, &mesh_.rotation_matrix);
mesh_.euler_angles = Geometry::extract_euler_angles(mesh_.rotation_matrix);
// find cool direction
if (mesh_.has_cooling_fan)
{
mesh_.orientation_vertical = orienter.find_cooling_direction2(mesh_.euler_angles, orienter.areas_cooling, mesh_.mesh);
BOOST_LOG_TRIVIAL(info) << "cooling direction: " << mesh_.orientation_vertical.transpose() << "\n";
Geometry::rotation_from_two_vectors(mesh_.orientation_vertical, mesh_.cooling_direction, mesh_.axis_vertical, mesh_.angle_vertical, &mesh_.rotation_matrix_vertical);
}
BOOST_LOG_TRIVIAL(debug) << "rotation_from_two_vectors: " << mesh_.orientation << "; " << mesh_.axis << "; " << mesh_.angle << "; euler: " << mesh_.euler_angles.transpose();
}});
}
@@ -539,6 +628,94 @@ void orient(ModelInstance* instance)
instance->rotate(rotation_matrix);
}
void orient_for_cooling(TriangleMesh& mesh, const FanDirection& fan_dir)
{
Vec3f best_direction{ 0, 0, 0 };
Vec3f machine_cool_dir{ 0, 0, 0 };
if (fan_dir == FanDirection::fdUndefine)
{
// no cooling fan, do not rotate along z axis
return;
}
else if (fan_dir == FanDirection::fdRight)
{
machine_cool_dir = { 1, 0, 0 }; // the cooling fan is on the right side.
}
else
{
// the cooling fan is on the left side or both side has cooling fans
machine_cool_dir = { -1, 0, 0 };
}
// 1. filter the overhang_areas
int nfaces = mesh.facets_count();
auto face_normals = its_face_normals(mesh.its);
Eigen::VectorXf normal_projection(nfaces, 1);
for (auto i = 0; i < nfaces; i++)
{
normal_projection(i) = face_normals[i].dot(Vec3f(0, 0, 1));
}
float angle_thres_high = -0.6427f;
float angle_thres_low = -0.97f;
// 2. compute the weighted overhang faces area
Eigen::VectorXf weighted_areas = Eigen::VectorXf::Zero(nfaces);
for (int i = 0; i < nfaces; i++)
{
if (normal_projection(i) < angle_thres_high && normal_projection(i) > angle_thres_low)
{
weighted_areas(i) = mesh.its.facet_area(i) * (1.0f - normal_projection(i));
}
}
const size_t sample_nums = 180;
std::vector<Vec3f> cool_dirs;
for (size_t i = 0; i < sample_nums; i++)
{
float angle_deg = i * (360.0 / sample_nums);
float angle_rad = angle_deg * (PI / 180.0);
cool_dirs.push_back(Vec3f{ std::cos(angle_rad), std::sin(angle_rad), 0 });
}
// 3. accumulate the weighted projected overhang area, find the max weighted project area direction
float overhang_projected_max = 0.f;
float overhang_projected_origin = 0.f;
for (auto cool_dir : cool_dirs)
{
float overhang_projected_tmp = 0.f;
for (size_t i = 0; i < nfaces; i++)
{
float cool_dir_projection = face_normals[i].dot(cool_dir);
if (weighted_areas[i] > 0 && cool_dir_projection > 0)
{
overhang_projected_tmp += weighted_areas[i] * cool_dir_projection;
}
}
if (overhang_projected_tmp > overhang_projected_max)
{
overhang_projected_max = overhang_projected_tmp;
best_direction = cool_dir;
}
if (cool_dir.dot(machine_cool_dir) > 0.999)
{
overhang_projected_origin = overhang_projected_tmp;
}
}
// The symmetric model has similar overhang projection at all angles, so Z-axis rotation is unnecessary.
if (std::abs(overhang_projected_origin - overhang_projected_max) < 1.0f)
{
return;
}
// rotate the mesh
Vec3d axis;
double angle;
Matrix3d rotation_matrix;
Geometry::rotation_from_two_vectors(best_direction.cast<double>(), machine_cool_dir.cast<double>(), axis, angle, &rotation_matrix);
mesh.rotate(angle, axis);
}
} // namespace arr
} // namespace Slic3r
+13 -2
View File
@@ -26,10 +26,18 @@ struct OrientMesh {
TriangleMesh mesh; /// The real mesh data
double overhang_angle = 30;
double angle{ 0 };
double angle_vertical{ 0 };
Vec3d axis{ 0,0,1 };
Vec3d axis_vertical{ 0,0,1 };
Vec3d orientation{ 0,0,1 };
Matrix3d rotation_matrix;
Vec3d euler_angles;
Vec3d orientation_vertical{ -1,0,0 };
Matrix3d rotation_matrix = Matrix3d::Identity();
Matrix3d rotation_matrix_vertical = Matrix3d::Identity();
Vec3d euler_angles = {0, 0, 0};
Vec3d euler_angles_vertical = {0, 0, 0};
Vec3d cooling_direction = {0, 0, 0};
bool has_cooling_fan{false};
std::string name;
/// Optional setter function which can store arbitrary data in its closure
@@ -154,6 +162,9 @@ void orient(ModelObject* obj);
void orient(ModelInstance* instance);
// rotate z axis for cooling
void orient_for_cooling(TriangleMesh& mesh, const FanDirection& fan_dir);
}} // namespace Slic3r::orientment
#endif // MODELORIENT_HPP
+16
View File
@@ -571,6 +571,19 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
return extrusion_coll;
}
// ORCA: only_one_wall_top detects the top as "slice − upper", so a feature rising from the middle of a
// top surface becomes an enclosed hole that gets ringed with extra inner walls. Fill those holes back
// into the top. Only holes that are both covered by the upper layer (excludes bridges) and backed by
// solid material (excludes voids) are filled.
static ExPolygons fill_enclosed_top_feature_holes(const ExPolygons &top, const Polygons &covered_by_upper, const ExPolygons &solid)
{
ExPolygons filled = top;
for (ExPolygon &ex : filled)
ex.holes.clear();
const ExPolygons feature_holes = intersection_ex(intersection_ex(diff_ex(filled, top), covered_by_upper), solid);
return feature_holes.empty() ? top : union_ex(top, feature_holes);
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
ExPolygons &non_top_polygons, ExPolygons &fill_clip) const {
// other perimeters
@@ -636,6 +649,8 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP
ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes);
ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes);
top_polygons = fill_enclosed_top_feature_holes(top_polygons, upper_polygons_series_clipped, orig_polygons);
// get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the
// min_width_top_surface we removed a bit before)
ExPolygons temp_gap = diff_ex(top_polygons, fill_clip);
@@ -2194,6 +2209,7 @@ void PerimeterGenerator::process_arachne()
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
top_expolygons = fill_enclosed_top_feature_holes(top_expolygons, upper_slices_clipped, infill_contour);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
+23
View File
@@ -712,6 +712,26 @@ namespace client
static void regex_matches (expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '=', lhs); }
static void regex_doesnt_match(expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '!', lhs); }
// Replace every match of the regular expression 'pattern' in the string 'subject' with 'replacement'.
// The replacement may reference capture groups ($1, $2, ...). Store the result into subject.
static void regex_replace(expr &subject, IteratorRange &pattern, expr &replacement)
{
if (subject.type() == TYPE_EMPTY)
// Inside an if / else block to be skipped
return;
if (subject.type() != TYPE_STRING)
subject.throw_exception("regex_replace() first parameter must be a string.");
try {
std::string re(++ pattern.begin(), -- pattern.end());
std::string result = SLIC3R_REGEX_NAMESPACE::regex_replace(subject.s(), SLIC3R_REGEX_NAMESPACE::regex(re), replacement.to_string());
subject.set_s(std::move(result));
} catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) {
// Syntax error in the regular expression
boost::throw_exception(qi::expectation_failure<Iterator>(
pattern.begin(), pattern.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what())));
}
}
static void one_of_test_init(expr &out) {
out.set_b(false);
}
@@ -2323,6 +2343,8 @@ namespace client
[ px::bind(&expr::digits<false>, _val, _2, _3) ]
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
[ px::bind(&expr::digits<true>, _val, _2, _3) ]
| (kw["regex_replace"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > regular_expression > ',' > conditional_expression(_r1) > ')')
[ px::bind(&expr::regex_replace, _val, _2, _3) ]
| (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ]
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
@@ -2404,6 +2426,7 @@ namespace client
("min")
("max")
("random")
("regex_replace")
("filament_change")
("repeat")
("round")
+73 -17
View File
@@ -252,7 +252,7 @@ void extend_default_config_length(DynamicPrintConfig& config, const bool set_nil
auto replace_nil_and_resize = [&](const std::string & key, int length){
ConfigOption* raw_ptr = config.option(key);
ConfigOptionVectorBase* opt_vec = static_cast<ConfigOptionVectorBase *>(raw_ptr);
if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && std::find(filament_extruder_override_keys.begin(), filament_extruder_override_keys.end(), key) == filament_extruder_override_keys.end()){
if(set_nil_to_default && raw_ptr->is_nil() && defaults.has(key) && !is_filament_extruder_override_key(key)){
opt_vec->clear();
opt_vec->resize(length, defaults.option(key));
}
@@ -890,12 +890,11 @@ std::string Preset::get_printer_type(PresetBundle *preset_bundle)
{
if (preset_bundle) {
auto config = &preset_bundle->printers.get_edited_preset().config;
std::string vendor_name;
for (auto vendor_profile : preset_bundle->vendors) {
for (auto vendor_model : vendor_profile.second.models)
if (vendor_model.name == config->opt_string("printer_model"))
const auto& printer_model = config->opt_string("printer_model");
for (const auto& vendor_profile : preset_bundle->vendors) {
for (const auto& vendor_model : vendor_profile.second.models)
if (vendor_model.name == printer_model)
{
vendor_name = vendor_profile.first;
return vendor_model.model_id;
}
}
@@ -907,11 +906,10 @@ std::string Preset::get_current_printer_type(PresetBundle *preset_bundle)
{
if (preset_bundle) {
auto config = &(this->config);
std::string vendor_name;
for (auto vendor_profile : preset_bundle->vendors) {
for (auto vendor_model : vendor_profile.second.models)
if (vendor_model.name == config->opt_string("printer_model")) {
vendor_name = vendor_profile.first;
const auto& printer_model = config->opt_string("printer_model");
for (const auto& vendor_profile : preset_bundle->vendors) {
for (const auto& vendor_model : vendor_profile.second.models)
if (vendor_model.name == printer_model) {
return vendor_model.model_id;
}
}
@@ -1044,9 +1042,16 @@ static std::vector<std::string> s_Preset_print_options{
"lightning_prune_angle",
"lightning_straightening_angle",
"top_surface_pattern",
"top_surface_expansion",
"top_surface_expansion_margin",
"top_surface_expansion_direction",
"bottom_surface_pattern",
"top_surface_fill_order",
"bottom_surface_fill_order",
"infill_direction",
"solid_infill_direction",
"top_layer_direction",
"bottom_layer_direction",
"counterbore_hole_bridging",
"infill_shift_step",
"sparse_infill_rotate_template",
@@ -1058,6 +1063,8 @@ static std::vector<std::string> s_Preset_print_options{
"skin_infill_density",
"align_infill_direction_to_model",
"extra_solid_infills",
"center_of_surface_pattern",
"separated_infills",
"minimum_sparse_infill_area",
"reduce_infill_retraction",
"internal_solid_infill_pattern",
@@ -1201,6 +1208,8 @@ static std::vector<std::string> s_Preset_print_options{
"wall_maximum_deviation",
"small_perimeter_speed",
"small_perimeter_threshold",
"small_support_perimeter_speed",
"small_support_perimeter_threshold",
"bridge_angle",
"internal_bridge_angle",
"relative_bridge_angle",
@@ -1271,6 +1280,7 @@ static std::vector<std::string> s_Preset_print_options{
"wipe_tower_bridging",
"wipe_tower_extra_flow",
"single_extruder_multi_material_priming",
"toolchange_ordering",
"wipe_tower_rotation_angle",
"tree_support_branch_distance_organic",
"tree_support_branch_diameter_organic",
@@ -1278,6 +1288,7 @@ static std::vector<std::string> s_Preset_print_options{
"hole_to_polyhole",
"hole_to_polyhole_threshold",
"hole_to_polyhole_twisted",
"hole_to_polyhole_max_edges",
"mmu_segmented_region_max_width",
"mmu_segmented_region_interlocking_depth",
"small_area_infill_flow_compensation",
@@ -1300,7 +1311,6 @@ static std::vector<std::string> s_Preset_print_options{
"interlocking_depth",
"interlocking_boundary_avoidance",
"interlocking_beam_width",
"calib_flowrate_topinfill_special_order",
// Z Anti-Aliasing (ZAA)
"zaa_enabled",
"zaa_minimize_perimeter_height",
@@ -1310,7 +1320,7 @@ static std::vector<std::string> s_Preset_print_options{
};
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
"filament_soluble", "filament_is_support", "filament_printable",
"filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility",
"filament_max_volumetric_speed", "filament_adaptive_volumetric_speed",
"filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
"filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume",
@@ -1326,8 +1336,20 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
//exhaust fan control
"activate_air_filtration","activate_air_filtration_during_print","activate_air_filtration_on_completion","during_print_exhaust_fan_speed","complete_print_exhaust_fan_speed",
// Retract overrides
"filament_retraction_length", "filament_z_hop", "filament_z_hop_types", "filament_retract_lift_above", "filament_retract_lift_below", "filament_retract_lift_enforce", "filament_retraction_speed", "filament_deretraction_speed", "filament_retract_restart_extra", "filament_retraction_minimum_travel",
"filament_retract_when_changing_layer", "filament_wipe", "filament_retract_before_wipe",
"filament_deretraction_speed",
"filament_retract_after_wipe", // Orca
"filament_retract_before_wipe",
"filament_retract_lift_above",
"filament_retract_lift_below",
"filament_retract_lift_enforce",
"filament_retract_restart_extra",
"filament_retract_when_changing_layer",
"filament_retraction_length",
"filament_retraction_minimum_travel",
"filament_retraction_speed",
"filament_wipe",
"filament_z_hop",
"filament_z_hop_types",
// Profile compatibility
"filament_vendor", "compatible_prints", "compatible_prints_condition", "compatible_printers", "compatible_printers_condition", "inherits",
//BBS
@@ -1345,7 +1367,13 @@ static std::vector<std::string> s_Preset_filament_options {/*"filament_colour",
"filament_multitool_ramming", "filament_multitool_ramming_volume", "filament_multitool_ramming_flow", "activate_chamber_temp_control", "chamber_minimal_temperature",
"filament_long_retractions_when_cut","filament_retraction_distances_when_cut", "idle_temperature",
//BBS filament change length while the extruder color
"filament_change_length","filament_flush_volumetric_speed","filament_flush_temp", "filament_cooling_before_tower",
"filament_change_length","filament_flush_volumetric_speed","filament_flush_temp","filament_flush_temp_fast", "filament_cooling_before_tower",
// Multi-nozzle pre-cooling / ramming / nozzle-change (nc) filament overrides
"filament_ramming_volumetric_speed", "filament_ramming_volumetric_speed_nc",
"filament_ramming_travel_time", "filament_ramming_travel_time_nc",
"filament_pre_cooling_temperature", "filament_pre_cooling_temperature_nc",
"filament_preheat_temperature_delta", "filament_retract_length_nc",
"filament_change_length_nc", "filament_prime_volume_nc",
"long_retractions_when_ec", "retraction_distances_when_ec",
"plugin_preference_overrides"
};
@@ -1357,6 +1385,8 @@ static std::vector<std::string> s_Preset_machine_limits_options {
"machine_min_extruding_rate", "machine_min_travel_rate",
"machine_max_jerk_x", "machine_max_jerk_y", "machine_max_jerk_z", "machine_max_jerk_e",
"machine_max_junction_deviation",
// Bedslinger mass/force limits
"machine_max_force_Y", "machine_bed_mass_Y", "machine_max_printed_mass",
//resonance avoidance ported from qidi slicer
"resonance_avoidance", "min_resonance_avoidance_speed", "max_resonance_avoidance_speed",
// Orca: input shaping
@@ -1374,7 +1404,7 @@ static std::vector<std::string> s_Preset_printer_options {
"default_print_profile", "inherits",
"silent_mode",
"scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode",
"nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure",
"nozzle_type", "nozzle_hrc","auxiliary_fan", "fan_direction", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","support_cooling_filter","cooling_filter_enabled","printer_structure","farthest_point_timelapse",
"best_object_pos", "head_wrap_detect_zone",
"host_type", "print_host", "printhost_apikey", "flashforge_serial_number", "bbl_use_printhost", "printer_agent",
"print_host_webui",
@@ -1387,6 +1417,12 @@ static std::vector<std::string> s_Preset_printer_options {
"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",
// Multi-nozzle count + pre-heat model printer options
"extruder_max_nozzle_count", "group_algo_with_time", "enable_pre_heating", "hotend_heating_rate", "hotend_cooling_rate",
"machine_hotend_change_time", "machine_prepare_compensation_time",
// Fast-purge printer flag + device/firmware-facing per-variant extruder-change
// deretraction speed (unconsumed by the slicer; carried by H2D/A2L/X2D/P2S machine profiles).
"support_fast_purge_mode", "deretract_speed_extruder_change",
"plugin_preference_overrides"
};
@@ -1862,6 +1898,26 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map<std
if (opt_src)
key_values[option] = opt_src->serialize();
}
// Orca: force-emit nullable filament override keys whenever they hold a nil ("off")
// value, even when the diff dropped them because the parent is nil too. Otherwise the
// key is absent from the synced profile and the cloud re-materializes it against the
// option's non-nil default (e.g. filament_retract_before_wipe -> 100%), silently
// resurrecting an override the user turned off. See GitHub issue on Retract Before Wipe.
if (m_type == Preset::TYPE_FILAMENT) {
for (const std::string& opt_key : filament_extruder_override_keys) {
if (key_values.count(opt_key))
continue; // already carried by the diff
const auto* opt_vec = dynamic_cast<const ConfigOptionVectorBase*>(preset.config.option(opt_key));
if (opt_vec == nullptr)
continue;
bool has_nil = false;
for (size_t i = 0; i < opt_vec->size(); ++i)
if (opt_vec->is_nil(i)) { has_nil = true; break; }
if (has_nil)
key_values[opt_key] = opt_vec->serialize();
}
}
}
else {
for (auto iter = preset.config.cbegin(); iter != preset.config.cend(); ++iter)
+2
View File
@@ -72,6 +72,7 @@
#define BBL_JSON_KEY_BOTTOM_TEXTURE_END_NAME "bottom_texture_end_name"
#define BBL_JSON_KEY_USE_DOUBLE_EXTRUDER_DEFAULT_TEXTURE "use_double_extruder_default_texture"
#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT "bottom_texture_rect"
#define BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER "bottom_texture_rect_longer"
#define BBL_JSON_KEY_MIDDLE_TEXTURE_RECT "middle_texture_rect"
#define BBL_JSON_KEY_HOTEND_MODEL "hotend_model"
@@ -150,6 +151,7 @@ public:
std::string bottom_texture_end_name;
std::string use_double_extruder_default_texture;
std::string bottom_texture_rect;
std::string bottom_texture_rect_longer;
std::string middle_texture_rect;
std::string hotend_model;
PrinterVariant* variant(const std::string &name) {
+194 -30
View File
@@ -52,9 +52,23 @@ static std::vector<std::string> s_project_options {
"wipe_tower_rotation_angle",
"curr_bed_type",
"flush_multiplier",
// Fast-purge mode: project-level purge control, inert at Default.
"flush_multiplier_fast",
"prime_volume_mode",
"nozzle_volume_type",
"filament_map_mode",
"filament_map"
"filament_map",
// Per-filament nozzle-volume choice; project-level like filament_map so the per-filament
// slot resolution survives preset switches.
"filament_volume_map",
// Per-filament physical-nozzle choice the grouping engine writes back; project-level so a
// saved project round-trips the assignment alongside filament_map/filament_volume_map.
"filament_nozzle_map",
// Filament Track Switch device state: whether the switch is installed and ready, and
// whether dynamic per-nozzle filament mapping is active. Persisted with the project and
// restored from a saved 3mf; reset to false on load and set true only by live device sync.
"has_filament_switcher",
"enable_filament_dynamic_map"
};
//Orca: add custom as default
@@ -71,7 +85,8 @@ DynamicPrintConfig PresetBundle::construct_full_config(
const DynamicPrintConfig& project_config,
std::vector<Preset>& in_filament_presets,
bool apply_extruder,
std::optional<std::vector<int>> filament_maps_new)
std::optional<std::vector<int>> filament_maps_new,
std::optional<std::vector<int>> filament_volume_maps_new)
{
DynamicPrintConfig &printer_config = in_printer_preset.config;
DynamicPrintConfig &print_config = in_print_preset.config;
@@ -86,12 +101,23 @@ DynamicPrintConfig PresetBundle::construct_full_config(
size_t num_filaments = in_filament_presets.size();
std::vector<int> filament_maps = out.option<ConfigOptionInts>("filament_map")->values;
std::vector<int> filament_volume_maps(num_filaments, (int)nvtStandard);
ConfigOptionInts* filament_volume_map_opt = out.option<ConfigOptionInts>("filament_volume_map");
if (filament_maps_new.has_value())
filament_maps = *filament_maps_new;
if (filament_volume_maps_new.has_value())
filament_volume_maps = *filament_volume_maps_new;
else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments)
filament_volume_maps = filament_volume_map_opt->values;
// in some middle state, they may be different
if (filament_maps.size() != num_filaments) {
filament_maps.resize(num_filaments, 1);
}
if (filament_volume_maps.size() != num_filaments) {
filament_volume_maps.resize(num_filaments, nvtStandard);
}
auto *extruder_diameter = dynamic_cast<const ConfigOptionFloats *>(out.option("nozzle_diameter"));
// Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector.
@@ -112,17 +138,34 @@ DynamicPrintConfig PresetBundle::construct_full_config(
inherits.emplace_back(print_inherits);
// BBS: update printer config related with variants
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 1, extruder_volume_type_count = 1;
bool different_extruder = false;
if (apply_extruder) {
out.update_values_to_printer_extruders(out, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
// update print config related with variants
out.update_values_to_printer_extruders(out, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
different_extruder = out.support_different_extruders(extruder_count);
extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
if ((extruder_count > 1) || different_extruder) {
// Orca: keep processing variant_1 before variant_2 here; variant_2 slots are resolved
// against the printer id/variant lists as rewritten by the variant_1 pass, and the
// composed values depend on that order. Note the order is load-bearing, not correct
// in general: the variant_2 pass reads the original full-width arrays through indices
// resolved on the shrunk lists, which mis-reads presets whose variant_2 columns differ
// per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing
// path composes variant_2 first and is unaffected; changing the order here would alter
// long-standing composed values, so any fix must re-baseline them.
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
// update print config related with variants
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
}
}
if (num_filaments <= 1) {
// BBS: update filament config related with variants
DynamicPrintConfig filament_config = in_filament_presets[0].config;
if (apply_extruder) filament_config.update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]);
out.apply(filament_config);
compatible_printers_condition.emplace_back(in_filament_presets[0].compatible_printers_condition());
compatible_prints_condition.emplace_back(in_filament_presets[0].compatible_prints_condition());
@@ -145,8 +188,8 @@ DynamicPrintConfig PresetBundle::construct_full_config(
filament_temp_configs.resize(num_filaments);
for (size_t i = 0; i < num_filaments; ++i) {
filament_temp_configs[i] = *(filament_configs[i]);
if (apply_extruder)
filament_temp_configs[i].update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]);
}
// loop through options and apply them to the resulting config.
@@ -221,6 +264,7 @@ DynamicPrintConfig PresetBundle::construct_full_config(
out.option<ConfigOptionString>("printer_settings_id", true)->value = in_printer_preset.name;
out.option<ConfigOptionStrings>("filament_ids", true)->values = filament_ids;
out.option<ConfigOptionInts>("filament_map", true)->values = filament_maps;
out.option<ConfigOptionInts>("filament_volume_map", true)->values = filament_volume_maps;
auto add_if_some_non_empty = [&out](std::vector<std::string> &&values, const std::string &key) {
bool nonempty = false;
@@ -348,7 +392,7 @@ PresetBundle::PresetBundle()
auto& default_config = this->filaments.default_preset().config;
for(const std::string& opt_key : default_config.keys()){
ConfigOption* opt = default_config.optptr(opt_key, false);
bool is_override_key = std::find(filament_extruder_override_keys.begin(),filament_extruder_override_keys.end(), opt_key) != filament_extruder_override_keys.end();
bool is_override_key = is_filament_extruder_override_key(opt_key);
if(!is_override_key || !opt->nullable())
continue;
opt->deserialize("nil",ForwardCompatibilitySubstitutionRule::Disable);
@@ -651,16 +695,16 @@ bool PresetBundle::use_bbl_network()
}
bool PresetBundle::use_bbl_device_tab() {
const auto cfg = printers.get_edited_preset().config;
if (!is_bbl_vendor())
if (!is_bbl_vendor()) {
return false;
}
if (use_bbl_network()) {
return true;
}
// Use bbl device tab if printhost webui url is not set
const auto cfg = printers.get_edited_preset().config;
// Use bbl device tab if printhost webui url is not set
return cfg.opt_string("print_host_webui").empty();
}
@@ -721,6 +765,8 @@ std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const
auto iter = std::find(compatible_printers.begin(), compatible_printers.end(), printer_name);
if (iter != compatible_printers.end() && config.has("filament_printable")) {
info.filament_printable = config.option<ConfigOptionInts>("filament_printable")->values[0];
if (config.has("filament_extruder_compatibility"))
info.set_filament_extruder_compatibility(config.option<ConfigOptionInts>("filament_extruder_compatibility")->values[0]);
return info;
}
}
@@ -2674,6 +2720,12 @@ void PresetBundle::update_selections(AppConfig &config)
std::vector<int> filament_maps(filament_colors.size(), 1);
project_config.option<ConfigOptionInts>("filament_map")->values = filament_maps;
std::vector<int> filament_nozzle_maps(filament_colors.size(), 0);
project_config.option<ConfigOptionInts>("filament_nozzle_map")->values = filament_nozzle_maps;
std::vector<int> filament_volume_maps(filament_colors.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
project_config.option<ConfigOptionInts>("filament_volume_map")->values = filament_volume_maps;
std::vector<std::string> extruder_ams_count_str;
if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) {
boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(","));
@@ -2818,6 +2870,12 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
std::vector<int> filament_maps(filament_colors.size(), 1);
project_config.option<ConfigOptionInts>("filament_map")->values = filament_maps;
std::vector<int> filament_nozzle_maps(filament_colors.size(), 0);
project_config.option<ConfigOptionInts>("filament_nozzle_map")->values = filament_nozzle_maps;
std::vector<int> filament_volume_maps(filament_colors.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
project_config.option<ConfigOptionInts>("filament_volume_map")->values = filament_volume_maps;
std::vector<std::string> extruder_ams_count_str;
if (config.has_printer_setting(initial_printer_profile_name, "extruder_ams_count")) {
boost::algorithm::split(extruder_ams_count_str, config.get_printer_setting(initial_printer_profile_name, "extruder_ams_count"), boost::algorithm::is_any_of(","));
@@ -2994,7 +3052,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector<std::string> ne
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
filament_color->resize(n);
// Sync filament multi colour
@@ -3004,6 +3063,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector<std::string> ne
}
filament_color_type->resize(n);
filament_map->values.resize(n, 1);
filament_nozzle_map->values.resize(n, 0);
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
// BBS set new filament color to new_color
@@ -3031,7 +3092,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
filament_color->resize(n);
// Sync filament multi colour
@@ -3041,6 +3103,8 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
}
filament_color_type->resize(n);
filament_map->values.resize(n, 1);
filament_nozzle_map->values.resize(n, 0);
filament_volume_map->values.resize(n, static_cast<int>(NozzleVolumeType::nvtStandard));
ams_multi_color_filment.resize(n);
//BBS set new filament color to new_color
@@ -3081,15 +3145,25 @@ void PresetBundle::update_num_filaments(unsigned int to_del_flament_id)
ConfigOptionStrings *filament_multi_color = project_config.option<ConfigOptionStrings>("filament_multi_colour");
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts* filament_nozzle_map = project_config.option<ConfigOptionInts>("filament_nozzle_map");
ConfigOptionInts* filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
if (filament_color->values.size() > to_del_flament_id) {
filament_color->values.erase(filament_color->values.begin() + to_del_flament_id);
if (filament_map->values.size() > to_del_flament_id) {
filament_map->values.erase(filament_map->values.begin() + to_del_flament_id);
}
if (filament_nozzle_map->values.size() > to_del_flament_id) {
filament_nozzle_map->values.erase(filament_nozzle_map->values.begin() + to_del_flament_id);
}
if (filament_volume_map->values.size() > to_del_flament_id) {
filament_volume_map->values.erase(filament_volume_map->values.begin() + to_del_flament_id);
}
}
else {
filament_color->values.resize(to_del_flament_id);
filament_map->values.resize(to_del_flament_id, 1);
filament_nozzle_map->values.resize(to_del_flament_id, 0);
filament_volume_map->values.resize(to_del_flament_id, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// lambda function to erase or resize the container
@@ -3312,6 +3386,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ConfigOptionStrings *filament_color = project_config.option<ConfigOptionStrings>("filament_colour");
ConfigOptionStrings *filament_color_type = project_config.option<ConfigOptionStrings>("filament_colour_type");
ConfigOptionInts * filament_map = project_config.option<ConfigOptionInts>("filament_map");
ConfigOptionInts * filament_volume_map = project_config.option<ConfigOptionInts>("filament_volume_map");
if (color_only) {
auto get_map_index = [&ams_infos](const std::vector<AMSMapInfo> &infos, const AMSMapInfo &temp) {
for (int i = 0; i < infos.size(); i++) {
@@ -3493,6 +3568,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
ams_multi_color_filment = exist_multi_color_filment;
this->filament_presets = exist_filament_presets;
filament_map->values.resize(exist_filament_presets.size(), 1);
filament_volume_map->values.resize(exist_filament_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
}
else {//overwrite;
bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(),
@@ -3547,12 +3623,14 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
this->filament_presets = result_presets;
ams_multi_color_filment = result_multi_colors;
filament_map->values.resize(total, 1);
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
} else {
// BBL: existing wholesale replace
filament_color->values = ams_filament_colors;
filament_color_type->values = ams_filament_color_types;
this->filament_presets = ams_filament_presets;
filament_map->values.resize(ams_filament_colors.size(), 1);
filament_volume_map->values.resize(ams_filament_colors.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
}
auto& print_config = this->prints.get_edited_preset().config;
@@ -3855,10 +3933,26 @@ bool PresetBundle::support_different_extruders() const
return supported;
}
DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional<std::vector<int>>filament_maps) const
std::vector<int> PresetBundle::get_default_nozzle_volume_types_for_filaments(std::vector<int>& f_maps)
{
std::vector<int> result;
int filament_count = f_maps.size();
result.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
auto opt_nozzle_volume_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(this->project_config.option("nozzle_volume_type"));
for (int index = 0; index < filament_count; index++)
{
if (opt_nozzle_volume_type && opt_nozzle_volume_type->values.size() > (f_maps[index] - 1))
result[index] = opt_nozzle_volume_type->values[f_maps[index] - 1];
}
return result;
}
DynamicPrintConfig PresetBundle::full_config(bool apply_extruder, std::optional<std::vector<int>>filament_maps, std::optional<std::vector<int>> filament_volume_maps) const
{
return (this->printers.get_edited_preset().printer_technology() == ptFFF) ?
this->full_fff_config(apply_extruder, filament_maps) :
this->full_fff_config(apply_extruder, filament_maps, filament_volume_maps) :
this->full_sla_config();
}
@@ -3872,16 +3966,52 @@ DynamicPrintConfig PresetBundle::full_config_secure(std::optional<std::vector<in
config.erase("printhost_cafile");
config.erase("printhost_user");
config.erase("printhost_password");
config.erase("printhost_port");
config.erase("printhost_port");
return config;
}
std::vector<std::vector<std::vector<float>>> PresetBundle::get_full_flush_matrix(bool with_multiplier) const
{
auto full_config = this->full_config();
int extruder_nums = full_config.option<ConfigOptionFloats>("nozzle_diameter")->values.size();
std::vector<double> flush_volume_value = full_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
int filament_nums = full_config.option<ConfigOptionStrings>("filament_type")->values.size();
std::vector<std::vector<std::vector<float>>> matrix;
for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) {
std::vector<float> flush_matrix(cast<float>(get_flush_volumes_matrix(flush_volume_value, extruder_id, extruder_nums)));
std::vector<std::vector<float>> wipe_volumes;
for (unsigned int i = 0; i < filament_nums; ++i)
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * filament_nums, flush_matrix.begin() + (i + 1) * filament_nums));
matrix.emplace_back(wipe_volumes);
}
if (with_multiplier) {
// Fast purge mode uses flush_multiplier_fast; the default prime_volume_mode==Default
// (or the key absent) reads flush_multiplier, so this is inert.
auto* mode_opt = project_config.option<ConfigOptionEnum<PrimeVolumeMode>>("prime_volume_mode");
const bool use_fast = mode_opt && mode_opt->value == PrimeVolumeMode::pvmFast;
auto* mult_opt = project_config.option<ConfigOptionFloats>(use_fast ? "flush_multiplier_fast" : "flush_multiplier");
auto flush_multiplies = mult_opt ? mult_opt->values : project_config.option<ConfigOptionFloats>("flush_multiplier")->values;
flush_multiplies.resize(extruder_nums, 1);
for (size_t extruder_id = 0; extruder_id < extruder_nums; ++extruder_id) {
for (auto& vec : matrix[extruder_id]) {
for (auto& v : vec)
v *= flush_multiplies[extruder_id];
}
}
}
return matrix;
}
const std::set<std::string> ignore_settings_list ={
"inherits",
"print_settings_id", "filament_settings_id", "printer_settings_id"
};
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new) const
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new, std::optional<std::vector<int>> filament_volume_maps_new) const
{
DynamicPrintConfig out;
out.apply(FullPrintConfig::defaults());
@@ -3895,8 +4025,17 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
size_t num_filaments = this->filament_presets.size();
std::vector<int> filament_maps = out.option<ConfigOptionInts>("filament_map")->values;
std::vector<int> filament_volume_maps(num_filaments, (int)nvtStandard);
ConfigOptionInts* filament_volume_map_opt = out.option<ConfigOptionInts>("filament_volume_map");
if (filament_maps_new.has_value())
filament_maps = *filament_maps_new;
if (filament_volume_maps_new.has_value()) {
filament_volume_maps = *filament_volume_maps_new;
out.option<ConfigOptionInts>("filament_volume_map", true)->values = filament_volume_maps;
}
else if (filament_volume_map_opt && filament_volume_map_opt->values.size() == num_filaments)
filament_volume_maps = filament_volume_map_opt->values;
//in some middle state, they may be different
if (filament_maps.size() != num_filaments) {
filament_maps.resize(num_filaments, 1);
@@ -3904,6 +4043,9 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
else {
assert(filament_maps.size() == num_filaments);
}
if (filament_volume_maps.size() != num_filaments) {
filament_volume_maps.resize(num_filaments, nvtStandard);
}
auto* extruder_diameter = dynamic_cast<const ConfigOptionFloats*>(out.option("nozzle_diameter"));
// Collect the "compatible_printers_condition" and "inherits" values over all presets (print, filaments, printers) into a single vector.
@@ -3933,18 +4075,34 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
different_settings.emplace_back(different_print_settings);
//BBS: update printer config related with variants
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 1, extruder_volume_type_count = 1;
bool different_extruder = false;
if (apply_extruder) {
out.update_values_to_printer_extruders(out, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
//update print config related with variants
out.update_values_to_printer_extruders(out, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
different_extruder = out.support_different_extruders(extruder_count);
extruder_volume_type_count = out.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
if ((extruder_count > 1) || different_extruder) {
// Orca: keep processing variant_1 before variant_2 here; variant_2 slots are resolved
// against the printer id/variant lists as rewritten by the variant_1 pass, and the
// composed values depend on that order. Note the order is load-bearing, not correct
// in general: the variant_2 pass reads the original full-width arrays through indices
// resolved on the shrunk lists, which mis-reads presets whose variant_2 columns differ
// per variant (e.g. X2D machine_max_speed_e/machine_max_acceleration_e). The slicing
// path composes variant_2 first and is unaffected; changing the order here would alter
// long-standing composed values, so any fix must re-baseline them.
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
//update print config related with variants
out.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
}
}
if (num_filaments <= 1) {
//BBS: update filament config related with variants
DynamicPrintConfig filament_config = this->filaments.get_edited_preset().config;
if (apply_extruder)
filament_config.update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_config.update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[0], (NozzleVolumeType)filament_volume_maps[0]);
out.apply(filament_config);
compatible_printers_condition.emplace_back(this->filaments.get_edited_preset().compatible_printers_condition());
compatible_prints_condition .emplace_back(this->filaments.get_edited_preset().compatible_prints_condition());
@@ -4037,8 +4195,8 @@ DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optio
filament_temp_configs.resize(num_filaments);
for (size_t i = 0; i < num_filaments; ++i) {
filament_temp_configs[i] = *(filament_configs[i]);
if (apply_extruder)
filament_temp_configs[i].update_values_to_printer_extruders(out, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i]);
if (apply_extruder && ((extruder_count > 1) || different_extruder))
filament_temp_configs[i].update_values_to_printer_extruders(out, extruder_count, extruder_volume_type_count, nozzle_volume_types, filament_options_with_variant, "", "filament_extruder_variant", 1, filament_maps[i], (NozzleVolumeType)filament_volume_maps[i]);
}
// loop through options and apply them to the resulting config.
@@ -4282,6 +4440,10 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
};
clear_compatible_printers(config);
// Dynamic per-nozzle filament mapping reflects live device state, not a stored setting;
// drop it from any imported config so it only comes from the connected printer.
config.erase("enable_filament_dynamic_map");
#if 0
size_t num_extruders = (printer_technology == ptFFF) ?
std::min(config.option<ConfigOptionFloats>("nozzle_diameter" )->values.size(),
@@ -4770,6 +4932,8 @@ std::pair<PresetsConfigSubstitutions, size_t> PresetBundle::load_vendor_configs_
model.use_double_extruder_default_texture = it.value();
} else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT)) {
model.bottom_texture_rect = it.value();
} else if (boost::iequals(it.key(), BBL_JSON_KEY_BOTTOM_TEXTURE_RECT_LONGER)) {
model.bottom_texture_rect_longer = it.value();
} else if (boost::iequals(it.key(), BBL_JSON_KEY_MIDDLE_TEXTURE_RECT)) {
model.middle_texture_rect = it.value();
}
@@ -5528,7 +5692,7 @@ void PresetBundle::set_default_suppressed(bool default_suppressed)
printers.set_default_suppressed(default_suppressed);
}
bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes, bool check_references) const
bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const
{
if (m_errors != 0 || printers.m_errors != 0 || filaments.m_errors != 0 || prints.m_errors != 0)
return true;
@@ -5551,7 +5715,7 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes, bool check
if (check_duplicate_filament_subtypes && this->check_duplicate_filament_subtypes())
has_errors = true;
if (check_references && this->check_preset_references())
if (this->check_preset_references())
has_errors = true;
return has_errors;
+34 -6
View File
@@ -72,6 +72,25 @@ struct FilamentBaseInfo
bool is_support{ false };
bool is_system{ true };
int filament_printable = 3;
// filament_extruder_compatibility packs one compatibility level per extruder into a single
// 32-bit int, 3 bits per extruder (up to 10 extruders). Levels: 0 = printable, 1 = error,
// 2 = critical warning, 3 = warning (4-7 reserved). extruder_id is 0-based.
int get_extruder_compatibility(int extruder_id) const {
constexpr int bits_per_extruder = 3;
constexpr int extruder_mask = (1 << bits_per_extruder) - 1; // 0x7
constexpr int max_extruder_count = 32 / bits_per_extruder; // 10
if (extruder_id < 0 || extruder_id >= max_extruder_count)
return 0;
return (m_filament_extruder_compatibility >> (bits_per_extruder * extruder_id)) & extruder_mask;
}
void set_filament_extruder_compatibility(int value) { m_filament_extruder_compatibility = value; }
int get_filament_extruder_compatibility() const { return m_filament_extruder_compatibility; }
private:
int m_filament_extruder_compatibility = 0;
};
enum BundleType{
@@ -156,7 +175,8 @@ public:
const DynamicPrintConfig &project_config,
std::vector<Preset> &in_filament_presets,
bool apply_extruder,
std::optional<std::vector<int>> filament_maps_new);
std::optional<std::vector<int>> filament_maps_new,
std::optional<std::vector<int>> filament_volume_maps_new = std::nullopt);
// ORCA: utility function to find the vendor for a given preset name
static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type);
@@ -364,10 +384,19 @@ public:
bool has_defauls_only() const
{ return prints.has_defaults_only() && filaments.has_defaults_only() && printers.has_defaults_only(); }
DynamicPrintConfig full_config(bool apply_extruder = true, std::optional<std::vector<int>>filament_maps = std::nullopt) const;
DynamicPrintConfig full_config(bool apply_extruder = true, std::optional<std::vector<int>>filament_maps = std::nullopt, std::optional<std::vector<int>> filament_volume_maps = std::nullopt) const;
// full_config() with the some "useless" config removed.
DynamicPrintConfig full_config_secure(std::optional<std::vector<int>>filament_maps = std::nullopt) const;
// Default per-filament nozzle-volume types: each filament inherits the volume type of the
// extruder it maps to (1-based f_maps), Standard when unknown.
std::vector<int> get_default_nozzle_volume_types_for_filaments(std::vector<int>& f_maps);
// Per-extruder flush matrix [extruder_id][from_filament][to_filament] in mm^3, optionally scaled
// by the per-extruder flush_multiplier (or flush_multiplier_fast when prime_volume_mode==Fast).
// Used by the print-dispatch nozzle-mapping flush-weight estimate.
std::vector<std::vector<std::vector<float>>> get_full_flush_matrix(bool with_multiplier = true) const;
//BBS: add some functions for multiple extruders
int get_printer_extruder_count() const;
bool support_different_extruders() const;
@@ -485,9 +514,8 @@ public:
return { Preset::TYPE_PRINTER, Preset::TYPE_SLA_PRINT, Preset::TYPE_SLA_MATERIAL };
}
// Orca: for validation only. The duplicate filament subtype and preset-reference checks are
// opt-in for now (enabled per-vendor by the profile-check CI as vendors are cleaned up).
bool has_errors(bool check_duplicate_filament_subtypes = false, bool check_preset_references = false) const;
// Orca: for validation only.
bool has_errors(bool check_duplicate_filament_subtypes = false) const;
// Orca: for validation only. Flag any system preset whose inherits / compatible_printers /
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
@@ -520,7 +548,7 @@ private:
/*ConfigSubstitutions load_config_file_config_bundle(
const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
DynamicPrintConfig full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps=std::nullopt) const;
DynamicPrintConfig full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps=std::nullopt, std::optional<std::vector<int>> filament_volume_maps=std::nullopt) const;
DynamicPrintConfig full_sla_config() const;
// Orca: used for validation only
+517 -20
View File
@@ -24,6 +24,7 @@
#include <algorithm>
#include <limits>
#include <numeric>
#include <unordered_map>
#include <unordered_set>
#include <sstream>
#include <boost/filesystem/path.hpp>
@@ -181,6 +182,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"filename_format",
"retraction_minimum_travel",
"retract_before_wipe",
// Orca:
"retract_after_wipe",
"retract_when_changing_layer",
"retraction_length",
"retract_length_toolchange",
@@ -214,6 +217,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"chamber_minimal_temperature",
"thumbnails",
"thumbnails_format",
"center_of_surface_pattern",
"separated_infills",
"seam_gap",
"role_based_wipe_speed",
"wipe_speed",
@@ -336,9 +341,13 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "first_layer_print_sequence"
|| opt_key == "other_layers_print_sequence"
|| opt_key == "other_layers_print_sequence_nums"
|| opt_key == "toolchange_ordering"
|| opt_key == "extruder_ams_count"
|| opt_key == "extruder_nozzle_stats"
|| opt_key == "filament_map_mode"
|| opt_key == "filament_map"
|| opt_key == "filament_nozzle_map"
|| opt_key == "filament_volume_map"
|| opt_key == "filament_adhesiveness_category"
|| opt_key == "filament_tower_interface_pre_extrusion_dist"
|| opt_key == "filament_tower_interface_pre_extrusion_length"
@@ -2550,6 +2559,9 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::vector<const PrintInstance*>::const_iterator print_object_instance_sequential_active;
std::vector<std::pair<coordf_t, std::vector<GCode::LayerToPrint>>> layers_to_print = GCode::collect_layers_to_print(*this);
std::vector<unsigned int> printExtruders;
// Cleared on every process so a print-sequence or selector-mode change can never leave
// stale object pointers behind; repopulated below only by the sequential selector path.
m_sequential_dynamic_orderings.clear();
if (this->config().print_sequence == PrintSequence::ByObject) {
// Order object instances for sequential print.
print_object_instances_ordering = sort_object_instances_by_model_order(*this);
@@ -2570,26 +2582,100 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
auto physical_unprintables = this->get_physical_unprintable_filaments(used_filaments);
auto geometric_unprintables = this->get_geometric_unprintable_filaments();
std::vector<int>filament_maps = this->get_filament_maps();
auto map_mode = get_filament_map_mode();
// get recommended filament map
if (map_mode < FilamentMapMode::fmmManual) {
filament_maps = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables);
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) { return value + 1; });
update_filament_maps_to_config(filament_maps);
auto filament_unprintable_volumes = this->get_filament_unprintable_flow(used_filaments);
// Selector (per-layer regroup) prints skip the static grouping: their print-wide result
// is stitched from the per-object plans after the ordering loop below.
const bool dynamic_reorder = this->is_dynamic_group_reorder();
if (!dynamic_reorder) {
std::vector<int>filament_maps = this->get_filament_maps();
auto map_mode = get_filament_map_mode();
// Grouping returns a nozzle-aware result; the 1-based extruder map for the by-object
// path is derived from it. It is computed in every static map mode (in manual modes it
// mirrors the user's assignment) and published print-wide: GCode's per-nozzle
// placeholder and config-index lookups read it via get_layered_nozzle_group_result(),
// and without it sequential exports on multi-nozzle printers see an empty nozzle table
// (e.g. nozzle_diameter_at_nozzle_id[]) and custom g-code fails to resolve.
auto grouping_result = ToolOrdering::get_recommended_filament_maps(all_filaments, this, map_mode, physical_unprintables, geometric_unprintables, filament_unprintable_volumes);
this->set_nozzle_group_result(std::make_shared<MultiNozzleUtils::LayeredNozzleGroupResult>(grouping_result));
// Orca: the sequential write-back stays gated to auto modes. In manual modes the
// config maps already carry the user's assignment (the per-object ToolOrdering below
// consumes them directly), so a write-back would only re-store the pre-slice values;
// keeping the gate avoids churning the config on every sequential manual slice.
if (map_mode < FilamentMapMode::fmmManual) {
auto derived_maps = grouping_result.get_extruder_map(false);
if (!derived_maps.empty()) {
filament_maps = derived_maps;
// Write the maps back: used filaments adopt the engine's extruder/nozzle
// choice, unused ones keep their config assignment.
// Orca: the config maps are the merge base; fall back to a synthesized base
// when no producer sized them to the filament count (CLI runs until the
// per-filament synthesis lands there), where indexing per filament would
// run out of bounds.
std::vector<int> base_filament_map = m_config.filament_map.values;
if (base_filament_map.size() != derived_maps.size())
base_filament_map.assign(derived_maps.size(), 1);
std::vector<int> base_volume_map = m_config.filament_volume_map.values;
if (base_volume_map.size() != derived_maps.size())
base_volume_map.assign(derived_maps.size(), (int)nvtStandard);
update_filament_maps_to_config(FilamentGroupUtils::update_used_filament_values(base_filament_map, derived_maps, used_filaments),
FilamentGroupUtils::update_used_filament_values(base_volume_map, grouping_result.get_volume_map(), used_filaments),
grouping_result.get_nozzle_map());
}
}
// check map valid both in auto and mannual mode
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; });
}
// check map valid both in auto and mannual mode
std::transform(filament_maps.begin(), filament_maps.end(), filament_maps.begin(), [](int value) {return value - 1; });
// print_object_instances_ordering = sort_object_instances_by_max_z(print);
const PrintObject *prev_planned_object = nullptr;
unsigned int seq_last_extruder = (unsigned int)-1;
MultiNozzleUtils::NozzleStatusRecorder nozzle_status;
std::vector<std::vector<int>> nozzle_map_per_layer;
std::vector<std::vector<unsigned int>> stitched_layer_filaments;
print_object_instance_sequential_active = print_object_instances_ordering.begin();
for (; print_object_instance_sequential_active != print_object_instances_ordering.end(); ++print_object_instance_sequential_active) {
tool_ordering = ToolOrdering(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
tool_ordering.sort_and_build_data(*(*print_object_instance_sequential_active)->print_object, initial_extruder_id);
const PrintObject *print_object = (*print_object_instance_sequential_active)->print_object;
if (dynamic_reorder) {
if (print_object != prev_planned_object) {
// Plan each unique object once, threading the physical nozzle occupancy and
// the previous object's last filament into the next plan; repeated instances
// of an object reuse the plan, mirroring the export loop's reuse.
ToolOrdering ordering(*print_object, seq_last_extruder);
ordering.set_nozzle_status(nozzle_status);
ordering.sort_and_build_data(*print_object, seq_last_extruder);
nozzle_status = ordering.get_nozzle_status();
if (ordering.last_extruder() != static_cast<unsigned int>(-1))
seq_last_extruder = ordering.last_extruder();
const auto &object_maps = ordering.get_layered_nozzle_group_result().get_layer_filament_nozzle_maps();
nozzle_map_per_layer.insert(nozzle_map_per_layer.end(), object_maps.begin(), object_maps.end());
// Orca: the stitch input comes from the same orderings that produced the
// per-layer maps — the collection loop above is per-instance and seeded -1,
// so its layers are misaligned with these plans. layer_tools() of a sorted
// ordering already carries the planned per-layer filament order.
for (const auto &layer_tool : ordering.layer_tools())
stitched_layer_filaments.emplace_back(layer_tool.extruders);
m_sequential_dynamic_orderings[print_object] = std::move(ordering);
prev_planned_object = print_object;
}
tool_ordering = m_sequential_dynamic_orderings.at(print_object);
} else {
tool_ordering = ToolOrdering(*print_object, initial_extruder_id);
tool_ordering.sort_and_build_data(*print_object, initial_extruder_id);
}
if ((initial_extruder_id = tool_ordering.first_extruder()) != static_cast<unsigned int>(-1)) {
append(printExtruders, tool_ordering.tools_for_layer(layers_to_print.front().first).extruders);
}
}
if (dynamic_reorder && m_objects.size() > 1) {
// Stitch the per-object plans into one print-wide selector result. A single-object
// sequential print publishes (and writes back) from its own ordering instead: the
// per-object publish gate treats one object as not sequential.
auto stitched = ToolOrdering::build_sequential_group_result(this, std::move(nozzle_map_per_layer), stitched_layer_filaments,
stitched_layer_filaments, used_filaments, physical_unprintables,
geometric_unprintables, filament_unprintable_volumes);
this->set_nozzle_group_result(std::make_shared<MultiNozzleUtils::LayeredNozzleGroupResult>(stitched));
update_to_config_by_nozzle_group_result(stitched);
}
}
else {
tool_ordering = this->tool_ordering();
@@ -2733,8 +2819,14 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
gcode.export_layer_filaments(result);
//BBS
if (result != nullptr)
if (result != nullptr) {
result->conflict_result = m_conflict_result;
// Surface the slicer's per-filament nozzle grouping onto the post-slice result
// the device GUI reads. This is the static L/R + rack subset the multi-nozzle path computes;
// null for single-nozzle prints where nothing computes it. It is assigned after g-code
// generation and read by no emitter, so it does not affect the emitted g-code.
result->nozzle_group_result = this->get_layered_nozzle_group_result();
}
return path.c_str();
}
@@ -3258,16 +3350,70 @@ void Print::finalize_first_layer_convex_hull()
m_first_layer_convex_hull = Geometry::convex_hull(m_first_layer_convex_hull.points);
}
void Print::update_filament_maps_to_config(std::vector<int> f_maps)
void Print::update_filament_maps_to_config(std::vector<int> f_maps, std::vector<int> f_volume_maps, std::vector<int> f_nozzle_maps)
{
if (m_config.filament_map.values != f_maps)
if ((m_config.filament_map.values != f_maps) || (m_config.filament_volume_map.values != f_volume_maps) || (m_config.filament_nozzle_map.values != f_nozzle_maps))
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament maps changed after pre-slicing.");
m_ori_full_print_config.option<ConfigOptionInts>("filament_map", true)->values = f_maps;
m_config.filament_map.values = f_maps;
if (!f_volume_maps.empty()) {
m_ori_full_print_config.option<ConfigOptionInts>("filament_volume_map", true)->values = f_volume_maps;
m_config.filament_volume_map.values = f_volume_maps;
}
else {
m_ori_full_print_config.option<ConfigOptionInts>("filament_volume_map", true)->values.resize(f_maps.size(), nvtStandard);
m_config.filament_volume_map.values.resize(f_maps.size(), nvtStandard);
}
if (!f_nozzle_maps.empty()) {
m_ori_full_print_config.option<ConfigOptionInts>("filament_nozzle_map", true)->values = f_nozzle_maps;
m_config.filament_nozzle_map.values = f_nozzle_maps;
}
}
{
int extruder_count = 1, extruder_volume_type_count = 1;
bool support_multi = m_ori_full_print_config.support_different_extruders(extruder_count);
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
extruder_volume_type_count = m_ori_full_print_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
//filament_map_2
// Orca: seed with 0-based extruder indices so the override keying below degenerates to the
// plain per-extruder slot when the rebuild loop is skipped; the loop overwrites every
// entry when it runs.
m_config.filament_map_2.values = f_maps;
for (auto& v : m_config.filament_map_2.values)
--v;
auto opt_extruder_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(m_ori_full_print_config.option("extruder_type"));
auto opt_nozzle_volume_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(m_ori_full_print_config.option("nozzle_volume_type"));
// Orca: the loop tolerates configs without the extruder options (unit tests, degenerate
// presets); the backfill and the override are bounds-checked because the change block above
// is skipped when the maps are unchanged, in which case the stored map may be shorter than
// the filament count.
auto* ori_volume_map = m_ori_full_print_config.option<ConfigOptionInts>("filament_volume_map", true);
for (int index = 0; opt_extruder_type && opt_nozzle_volume_type && index < f_maps.size(); index++)
{
ExtruderType extruder_type = (ExtruderType)(opt_extruder_type->get_at(f_maps[index] - 1));
NozzleVolumeType nozzle_volume_type = (NozzleVolumeType)(opt_nozzle_volume_type->get_at(f_maps[index] - 1));
if (f_volume_maps.empty()) {
// No per-filament map supplied: backfill from the extruder's own volume type.
if (m_config.filament_volume_map.values.size() > index)
m_config.filament_volume_map.values[index] = nozzle_volume_type;
if (ori_volume_map->values.size() > index)
ori_volume_map->values[index] = nozzle_volume_type;
}
else if ((extruder_volume_type_count > extruder_count) && (m_config.filament_volume_map.values.size() > index))
nozzle_volume_type = (NozzleVolumeType)(m_config.filament_volume_map.values[index]);
m_config.filament_map_2.values[index] = m_ori_full_print_config.get_index_for_extruder(f_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
}
m_full_print_config = m_ori_full_print_config;
m_full_print_config.update_values_to_printer_extruders_for_multiple_filaments(m_full_print_config, filament_options_with_variant, "filament_self_index", "filament_extruder_variant");
std::set<std::string> filament_keys = filament_options_with_variant;
filament_keys.insert("filament_self_index");
if ((extruder_count > 1) || support_multi)
m_full_print_config.update_values_to_printer_extruders_for_multiple_filaments(m_full_print_config, extruder_count, extruder_volume_type_count, filament_keys, "filament_self_index", "filament_extruder_variant");
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
@@ -3280,16 +3426,147 @@ void Print::update_filament_maps_to_config(std::vector<int> f_maps)
const ConfigOption *opt_old_machine = m_config.option(opt_key);
if (opt_new_filament)
compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, f_maps);
compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, m_config.filament_map_2.values);
}
t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end());
m_config.apply_only(m_full_print_config, keys, true);
if ((extruder_count > 1) || support_multi) {
t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end());
keys.push_back("filament_self_index");
m_config.apply_only(m_full_print_config, keys, true);
}
if (!print_diff.empty()) {
m_placeholder_parser.apply_config(filament_overrides);
m_config.apply(filament_overrides);
}
}
update_filament_self_index_cache();
m_has_auto_filament_map_result = true;
}
bool Print::collect_filament_variant_uses(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result,
const DynamicPrintConfig& config,
std::unordered_map<int, std::vector<FilamentVariantUse>>& uses) const
{
auto opt_filament_type = config.option<ConfigOptionStrings>("filament_type");
auto opt_extruder_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(config.option("extruder_type"));
if (!opt_filament_type || !opt_extruder_type)
return false;
const size_t filament_count = opt_filament_type->values.size();
const size_t extruder_count = opt_extruder_type->values.size();
auto add_use = [&](std::set<FilamentVariantUse> &variant_set, const MultiNozzleUtils::NozzleInfo &nozzle) {
// Orca: a persisted result can outlive a printer swap; never index the extruder
// arrays with a stale nozzle record.
if (nozzle.extruder_id < 0 || static_cast<size_t>(nozzle.extruder_id) >= extruder_count)
return;
FilamentVariantUse use;
use.extruder_type = static_cast<ExtruderType>(opt_extruder_type->get_at(nozzle.extruder_id));
use.nozzle_volume_type = nozzle.volume_type;
use.extruder_id = nozzle.extruder_id;
variant_set.insert(use);
};
for (size_t f_index = 0; f_index < filament_count; ++f_index) {
std::set<FilamentVariantUse> variant_set;
for (const MultiNozzleUtils::NozzleInfo &nozzle : group_result.get_nozzles_for_filament(static_cast<int>(f_index)))
add_use(variant_set, nozzle);
// A filament the plan never routes (not printed) still needs a deterministic slot: take
// its default-map assignment from the result itself, so both the slice-time write-back
// and the apply-time reproduction resolve the same slot even when the surrounding
// filament_map has not round-tripped through the plate config in between.
if (variant_set.empty()) {
if (auto default_nozzle = group_result.get_nozzle_for_filament(static_cast<int>(f_index), -1); default_nozzle.has_value())
add_use(variant_set, *default_nozzle);
}
// Filaments still without a variant stay absent from the map: the slot rebuild then
// resolves them from their static filament_map / filament_volume_map assignment.
if (!variant_set.empty())
uses[static_cast<int>(f_index)] = std::vector<FilamentVariantUse>(variant_set.begin(), variant_set.end());
}
return true;
}
void Print::update_to_config_by_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result)
{
std::vector<int> derived_maps = group_result.get_extruder_map(false); // 1-based
if (derived_maps.empty())
return;
if (!group_result.is_support_dynamic_nozzle_map()) {
// No filament actually migrated between nozzles, so the plan reduces to a single
// grouping: write all maps like the static paths do, and the next apply re-derives
// identical slots from the written maps.
std::vector<int> base_filament_map = m_config.filament_map.values;
if (base_filament_map.size() != derived_maps.size())
base_filament_map.assign(derived_maps.size(), 1);
std::vector<int> base_volume_map = m_config.filament_volume_map.values;
if (base_volume_map.size() != derived_maps.size())
base_volume_map.assign(derived_maps.size(), (int)nvtStandard);
const std::vector<unsigned int> used_filaments = group_result.get_used_filaments();
update_filament_maps_to_config(FilamentGroupUtils::update_used_filament_values(base_filament_map, derived_maps, used_filaments),
FilamentGroupUtils::update_used_filament_values(base_volume_map, group_result.get_volume_map(), used_filaments),
group_result.get_nozzle_map());
return;
}
// Orca: keep the coarse per-filament extruder map published even though the per-layer truth
// lives in the grouping result: the pre-export consumers, the plate read-back after slicing
// and the preview panel all key on filament_map. The write is direct — the full map
// write-back's single-slot rebuild would undo the per-variant expansion below.
m_ori_full_print_config.option<ConfigOptionInts>("filament_map", true)->values = derived_maps;
m_config.filament_map.values = derived_maps;
std::unordered_map<int, std::vector<FilamentVariantUse>> filament_variant_uses;
if (!collect_filament_variant_uses(group_result, m_ori_full_print_config, filament_variant_uses)) {
// Degenerate config (no filament/extruder typing): fall back to the single-slot
// write-back so the maps and overrides stay coherent.
update_filament_maps_to_config(derived_maps);
return;
}
int extruder_count = 1, extruder_volume_type_count = 1;
m_ori_full_print_config.support_different_extruders(extruder_count);
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
extruder_volume_type_count = m_ori_full_print_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
// Note: filament_map_2 keeps its apply-time (static) derivation here; the per-slot machine
// indices below key the override merge instead, so nothing on this path reads it. Its other
// consumers are the three-map write-back (which recomputes it) and the diagnostic copy in
// the g-code header; the time estimator resolves per-(extruder x volume-type) machine limits
// from the live nozzle occupancy instead (see GCodeProcessor::get_machine_config_idx).
m_full_print_config = m_ori_full_print_config;
std::set<std::string> filament_keys = filament_options_with_variant;
filament_keys.insert("filament_self_index");
std::vector<int> slot_machine_indices;
m_full_print_config.update_filament_config_values_for_multiple_extruders(m_full_print_config, filament_variant_uses,
extruder_count, extruder_volume_type_count,
filament_keys, "filament_self_index", "filament_extruder_variant",
&slot_machine_indices);
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
t_config_option_keys print_diff;
DynamicPrintConfig filament_overrides;
for (auto& opt_key: extruder_retract_keys)
{
const ConfigOption *opt_new_filament = m_full_print_config.option(filament_prefix + opt_key);
const ConfigOption *opt_new_machine = m_full_print_config.option(opt_key);
const ConfigOption *opt_old_machine = m_config.option(opt_key);
if (opt_new_filament)
compute_filament_override_value(opt_key, opt_old_machine, opt_new_machine, opt_new_filament, m_full_print_config, print_diff, filament_overrides, slot_machine_indices);
}
{
t_config_option_keys keys(filament_options_with_variant.begin(), filament_options_with_variant.end());
keys.push_back("filament_self_index");
m_config.apply_only(m_full_print_config, keys, true);
}
if (!print_diff.empty()) {
m_placeholder_parser.apply_config(filament_overrides);
m_config.apply(filament_overrides);
}
update_filament_self_index_cache();
m_has_auto_filament_map_result = true;
}
@@ -3303,6 +3580,16 @@ std::vector<int> Print::get_filament_maps() const
return m_config.filament_map.values;
}
std::vector<int> Print::get_filament_nozzle_maps() const
{
return m_config.filament_nozzle_map.values;
}
std::vector<int> Print::get_filament_volume_maps() const
{
return m_config.filament_volume_map.values;
}
FilamentMapMode Print::get_filament_map_mode() const
{
return m_config.filament_map_mode;
@@ -3342,6 +3629,41 @@ std::vector<std::set<int>> Print::get_physical_unprintable_filaments(const std::
return physical_unprintables;
}
std::map<int, std::set<NozzleVolumeType>> Print::get_filament_unprintable_flow(const std::vector<unsigned int> &used_filaments) const
{
std::map<int, std::set<NozzleVolumeType>> ret;
std::vector<std::string> extruder_variant_list = m_config.printer_extruder_variant.values;
// A filament that declares no extruder variants carries no flow restriction.
const ConfigOptionStrings *filament_variant_opt = m_ori_full_print_config.option<ConfigOptionStrings>("filament_extruder_variant");
if (filament_variant_opt == nullptr)
return ret;
std::vector<std::string> filament_variant_list = filament_variant_opt->values;
std::vector<int> filament_self_index;
if (!m_ori_full_print_config.has("filament_self_index"))
filament_self_index.resize(filament_variant_list.size(), 1);
else
filament_self_index = m_ori_full_print_config.option<ConfigOptionInts>("filament_self_index")->values;
std::unordered_set<int> used_fils_set(used_filaments.begin(), used_filaments.end());
std::unordered_map<int, std::set<NozzleVolumeType>> filament_variant_map;
for(int i = 0; i < filament_variant_list.size(); ++i){
NozzleVolumeType volume = convert_to_nvt_type(filament_variant_list[i]);
if(volume != nvtHybrid) filament_variant_map[filament_self_index[i]].insert(volume);
}
for (auto iter : filament_variant_map) {
int fil_idx = iter.first - 1;
if (used_fils_set.find(fil_idx) == used_fils_set.end()) continue;
const std::set<NozzleVolumeType> &volumes = iter.second;
for (int exd_idx = 0; exd_idx < extruder_variant_list.size(); ++exd_idx) {
auto exd_volume = convert_to_nvt_type(extruder_variant_list[exd_idx]);
assert(exd_volume != nvtHybrid);
if (volumes.find(exd_volume) == volumes.end() && exd_volume != nvtHybrid) ret[fil_idx].insert(exd_volume);
}
}
return ret;
}
std::vector<double> Print::get_extruder_printable_height() const
{
@@ -3381,6 +3703,158 @@ size_t Print::get_extruder_id(unsigned int filament_id) const
return 0;
}
// Region reachable by every extruder = intersection of all per-extruder printable areas.
// For single-nozzle printers, or whenever extruder_printable_area is unpopulated / degenerate (all
// current single/dual profiles), fall back to the full printable_area so the wipe-tower-center clamp
// is identical to the previous full-bed clamp.
Polygons Print::get_extruder_shared_printable_polygon() const
{
const std::vector<Vec2ds>& extruder_printable_areas = m_config.extruder_printable_area.values;
if (m_config.nozzle_diameter.size() < 2 || extruder_printable_areas.empty())
return {Polygon::new_scale(m_config.printable_area.values)};
for (const Vec2ds& area : extruder_printable_areas)
if (area.size() < 3)
return {Polygon::new_scale(m_config.printable_area.values)};
Polygons shared_printable_polys = {Polygon::new_scale(extruder_printable_areas.front())};
for (size_t i = 1; i < extruder_printable_areas.size(); ++i)
shared_printable_polys = intersection(shared_printable_polys, Polygons{Polygon::new_scale(extruder_printable_areas[i])});
return shared_printable_polys;
}
// Narrow the stored grouping result to the layer-aware type the slicing pipeline uses.
std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> Print::get_layered_nozzle_group_result() const
{
return std::dynamic_pointer_cast<MultiNozzleUtils::LayeredNozzleGroupResult>(m_nozzle_group_result);
}
// Dynamic (per-layer selector) regroup predicate.
// Orca: enable_filament_dynamic_map is a project flag registered in the ConfigDef but NOT a static
// PrintConfig member, so it is read from the applied full config. No profile sets it; it is turned
// on per project by the "smart filament assign" checkbox (shown when a filament track switch is
// ready), so absent-key -> nullptr -> false keeps the static grouping path (identical output) for
// everything else. There is no mixed-colour-filament guard (mixed-colour filaments are not
// supported). The remaining gates (auto-for-flush mode, multi-extruder machine) read the static
// PrintConfig members.
bool Print::is_dynamic_group_reorder() const
{
const auto *opt = m_full_print_config.option<ConfigOptionBool>("enable_filament_dynamic_map");
const bool enabled = opt && opt->value;
if (!enabled || m_config.filament_map_mode != FilamentMapMode::fmmAutoForFlush || m_config.nozzle_diameter.size() <= 1)
return false;
return true;
}
int Print::get_filament_config_indx(int filament_id, int layer_id)
{
return get_config_index(filament_id, layer_id, m_config.filament_extruder_variant.values, m_filament_self_index, m_filament_index_map);
}
void Print::update_filament_self_index_cache()
{
m_missing_nozzle_group_logged.clear(); // reset the per-slice get_config_index log dedupe
std::vector<int> values;
if (m_full_print_config.has("filament_self_index")) {
values = m_full_print_config.option<ConfigOptionInts>("filament_self_index")->values;
} else if (m_ori_full_print_config.has("filament_self_index")) {
values = m_ori_full_print_config.option<ConfigOptionInts>("filament_self_index")->values;
} else {
values = m_config.filament_self_index.values;
}
size_t expected_size = m_config.filament_extruder_variant.values.size();
m_filament_self_index.clear();
if (expected_size == 0) {
m_filament_index_map.clear();
m_nozzle_index_map.clear();
return;
}
m_filament_self_index.resize(expected_size, 1);
if (!values.empty()) {
for (size_t i = 0; i < expected_size; ++i) {
int v = i < values.size() ? values[i] : 1;
if (v <= 0)
v = 1;
m_filament_self_index[i] = v;
}
}
m_filament_index_map.clear();
m_nozzle_index_map.clear();
}
int Print::get_nozzle_config_index(int filament_id, int layer_id)
{
// Orca: print_extruder_id/print_extruder_variant are PrintRegionConfig members in this codebase;
// the process-wide expanded values live in the default region config (regions never override them).
return get_config_index(filament_id, layer_id, m_default_region_config.print_extruder_variant.values, m_default_region_config.print_extruder_id.values, m_nozzle_index_map);
}
int Print::get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, FilamentIndexMap &index_map)
{
auto group_result = get_layered_nozzle_group_result();
// Orca: defensive — when no grouping producer has published a result yet, fall back to the
// static identity: one filament-variant column per filament.
if (!group_result)
return filament_id;
auto nozzle_info = group_result->get_nozzle_for_filament(filament_id, layer_id);
if (!nozzle_info.has_value()) {
// Orca: this fallback runs per-filament/per-layer in the g-code hot path — log once per filament
// (reset each slice) instead of flooding thousands of identical lines that bury the real error.
if (m_missing_nozzle_group_logged.insert(filament_id).second)
BOOST_LOG_TRIVIAL(error) << __FUNCTION__
<< boost::format(", Line %1%: could not found group_nozzle_info corresponding to filament_id %2%, layer_id %3% (further occurrences for this filament suppressed)") % __LINE__ % filament_id %
layer_id;
return 0;
}
ExtruderType extruder_type = ExtruderType(m_config.extruder_type.get_at(nozzle_info->extruder_id));
NozzleVolumeType nozzle_volume_type = nozzle_info->volume_type;
FilamentIndexKey key{filament_id, extruder_type, nozzle_volume_type};
auto iter = index_map.find(key);
if (iter == index_map.end()) {
int index = get_config_index_base(nozzle_volume_type, extruder_type, filament_id + 1, variant_list, self_index_list);
index_map[key] = index;
return index;
} else {
return index_map[key];
}
}
int Print::get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, PrintIndexMap &index_map)
{
auto group_result = get_layered_nozzle_group_result();
// Orca: same static fallback as the filament overload; the slot degenerates to the filament's
// extruder column (filament_map is 1 based, get_extruder_id guards the filament id range).
if (!group_result)
return (int)get_extruder_id(filament_id);
auto nozzle_info = group_result->get_nozzle_for_filament(filament_id, layer_id);
if (!nozzle_info.has_value()) {
// Orca: this fallback runs per-filament/per-layer in the g-code hot path — log once per filament
// (reset each slice) instead of flooding thousands of identical lines that bury the real error.
if (m_missing_nozzle_group_logged.insert(filament_id).second)
BOOST_LOG_TRIVIAL(error) << __FUNCTION__
<< boost::format(", Line %1%: could not found group_nozzle_info corresponding to filament_id %2%, layer_id %3% (further occurrences for this filament suppressed)") % __LINE__ % filament_id %
layer_id;
return 0;
}
int extruder_id = nozzle_info->extruder_id + 1; // to 1 based
ExtruderType extruder_type = ExtruderType(m_config.extruder_type.get_at(nozzle_info->extruder_id));
NozzleVolumeType nozzle_volume_type = nozzle_info->volume_type;
PrintIndexKey key{filament_id, extruder_id, extruder_type, nozzle_volume_type};
auto iter = index_map.find(key);
if (iter == index_map.end()) {
int index = get_config_index_base(nozzle_volume_type, extruder_type, extruder_id, variant_list, self_index_list);
index_map[key] = index;
return index;
} else {
return index_map[key];
}
}
// Wipe tower support.
bool Print::has_wipe_tower() const
{
@@ -3541,6 +4015,14 @@ void Print::_make_wipe_tower()
m_wipe_tower_data.tool_ordering.empty() ? 0.f : m_wipe_tower_data.tool_ordering.back().print_z, m_wipe_tower_data.tool_ordering.all_extruders());
wipe_tower.set_has_tpu_filament(this->has_tpu_filament());
wipe_tower.set_filament_map(this->get_filament_maps());
// Feed the has_filament_switcher device flag (develop-only dynamic key, read defensively from
// the full config — no shipping profile sets it) and the shared printable bed used by the PETG
// pre-extrusion offset clamp. Both are inert unless has_filament_switcher is set.
{
const ConfigOptionBool* hfs = m_full_print_config.option<ConfigOptionBool>("has_filament_switcher");
wipe_tower.set_has_filament_switcher(hfs && hfs->value);
}
wipe_tower.set_shared_print_bed(this->get_extruder_shared_printable_polygon());
// Set the extruder & material properties at the wipe tower object.
for (size_t i = 0; i < number_of_extruders; ++i)
wipe_tower.set_extruder(i, m_config);
@@ -3592,7 +4074,10 @@ void Print::_make_wipe_tower()
float volume_to_purge = 0;
if (pre_filament_id != (unsigned int)(-1) && pre_filament_id != filament_id) {
volume_to_purge = multi_extruder_flush[nozzle_id][pre_filament_id][filament_id];
volume_to_purge *= m_config.flush_multiplier.get_at(nozzle_id);
// Fast purge mode uses flush_multiplier_fast; Default is inert.
float flush_multiplier = (m_config.prime_volume_mode == PrimeVolumeMode::pvmFast) ? m_config.flush_multiplier_fast.get_at(nozzle_id)
: m_config.flush_multiplier.get_at(nozzle_id);
volume_to_purge *= flush_multiplier;
volume_to_purge = pre_filament_id == -1 ? 0 :
layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_filament_id, filament_id, volume_to_purge);
}
@@ -3601,8 +4086,10 @@ void Print::_make_wipe_tower()
float grab_purge_volume = m_config.grab_length.get_at(nozzle_id) * 2.4; //(diameter/2)^2*PI=2.4
volume_to_purge = std::max(0.f, volume_to_purge - grab_purge_volume);
// Saving mode reduces the prime volume to 15 mm3; Default is inert.
float prime_volume = (m_config.prime_volume_mode == PrimeVolumeMode::pvmSaving) ? 15.f : (float) m_config.prime_volume;
wipe_tower.plan_toolchange((float)layer_tools.print_z, (float)layer_tools.wipe_tower_layer_height, current_filament_id, filament_id,
m_config.prime_volume, volume_to_purge);
prime_volume, volume_to_purge);
current_filament_id = filament_id;
nozzle_cur_filament_ids[nozzle_id] = filament_id;
}
@@ -3856,10 +4343,20 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
const Vec3d origin = this->get_plate_origin();
processor.set_xy_offset(origin(0), origin(1));
// Reloaded sliced projects re-estimate with the same nozzle-grouping slot context as the
// original export; process_file re-derives the device-side nozzle grouping onto the result
// (via ensure_nozzle_group_result), so the multi-nozzle send/monitor mapping survives here.
if (result != nullptr && result->nozzle_group_result)
processor.initialize_from_context(result->nozzle_group_result);
//processor.enable_producers(true);
processor.process_file(file);
// filament seq is loaded from file, processor result will override the value
auto filament_seq_loaded = result->filament_change_sequence;
auto nozzle_seq_loaded = result->nozzle_change_sequence;
*result = std::move(processor.extract_result());
result->filament_change_sequence = filament_seq_loaded;
result->nozzle_change_sequence = nozzle_seq_loaded;
} catch (std::exception & /* ex */) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
throw Slic3r::RuntimeError(
+139 -5
View File
@@ -23,6 +23,7 @@
#include <functional>
#include <set>
#include <unordered_map>
#include "calib.hpp"
@@ -38,6 +39,7 @@ class SupportLayer;
class TreeSupportData;
class TreeSupport;
class ExtrusionLayers;
namespace MultiNozzleUtils { class NozzleGroupResultBase; class LayeredNozzleGroupResult; }
#define MAX_OUTER_NOZZLE_DIAMETER 4
// BBS: move from PrintObjectSlice.cpp
@@ -426,7 +428,7 @@ public:
// (layer height, first layer height, raft settings, print nozzle diameter etc).
const SlicingParameters& slicing_parameters() const { return m_slicing_params; }
// Orca: XYZ shrinkage compensation has introduced the const Vec3d &object_shrinkage_compensation parameter to the function below
static SlicingParameters slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation);
static SlicingParameters slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation, std::vector<int> variant_index = std::vector<int>());
size_t num_printing_regions() const throw() { return m_shared_regions->all_regions.size(); }
const PrintRegion& printing_region(size_t idx) const throw() { return *m_shared_regions->all_regions[idx].get(); }
@@ -497,7 +499,7 @@ public:
// If ! m_slicing_params.valid, recalculate.
void update_slicing_parameters();
static PrintObjectConfig object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders);
static PrintObjectConfig object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders, std::vector<int>& variant_index);
private:
void make_perimeters();
@@ -780,6 +782,7 @@ struct WipeTowerData
number_of_toolchanges = -1;
depth = 0.f;
brim_width = 0.f;
height = 0.f;
rib_offset = Vec2f::Zero();
wipe_tower_mesh_data = std::nullopt;
}
@@ -1016,15 +1019,50 @@ public:
const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const;
const ToolOrdering& tool_ordering() const { return m_tool_ordering; }
void update_filament_maps_to_config(std::vector<int> f_maps);
void update_filament_maps_to_config(std::vector<int> f_maps, std::vector<int> f_volume_maps = std::vector<int>{}, std::vector<int> f_nozzle_maps = std::vector<int>{});
// Write-back for a selector (per-layer planned) grouping result. When a filament actually
// migrates between nozzle variants, rebuilds the per-slot filament arrays so it holds one
// slot per variant and recomputes the extruder retract overrides against the expanded
// slots — update_filament_maps_to_config's single-slot rebuild cannot represent a
// migration. A result without migration reduces to a single grouping and takes the
// three-map write-back like the static paths.
void update_to_config_by_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result);
void apply_config_for_render(const DynamicConfig &config);
// 1 based group ids
std::vector<int> get_filament_maps() const;
FilamentMapMode get_filament_map_mode() const;
std::vector<int> get_filament_volume_maps() const;
std::vector<int> get_filament_nozzle_maps() const;
// get the group label of filament
size_t get_extruder_id(unsigned int filament_id) const;
// The region every extruder can reach,
// i.e. the intersection of all per-extruder printable areas. Falls back to the full printable_area
// for single-nozzle printers and whenever extruder_printable_area is not populated (all current
// single/dual profiles), so the wipe-tower-center clamp is byte-identical to full-bed clamping there.
Polygons get_extruder_shared_printable_polygon() const;
// Logical (extruder, nozzle) grouping result produced by ToolOrdering during reorder.
// Consumed by GCode via get_layered_nozzle_group_result()->get_nozzle_id(filament, layer) etc.
void set_nozzle_group_result(std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> result) { m_nozzle_group_result = result; }
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> get_nozzle_group_result() const { return m_nozzle_group_result; }
std::shared_ptr<MultiNozzleUtils::LayeredNozzleGroupResult> get_layered_nozzle_group_result() const;
// True only when the project opts into the per-layer filament selector
// (enable_filament_dynamic_map) in auto-for-flush mode on a multi-extruder machine. Gates the
// dynamic (per-layer) regroup branch in ToolOrdering::reorder_extruders_for_minimum_flush_volume,
// the sequential (by-object) plan stitching in Print::process, and GCode's use of the cached
// sequential plans. No profile sets the flag, so the static grouping path (byte-identical
// output) is the only one taken unless the user enables the selector.
bool is_dynamic_group_reorder() const;
// Per-object tool orderings planned by the sequential (by-object) selector regroup with
// cross-object nozzle-status threading. GCode export must consume these exact plans: a fresh
// per-object construction would re-plan from a different seed and diverge from the published
// stitched result. Empty on the static path.
const std::map<const PrintObject*, ToolOrdering>& sequential_dynamic_orderings() const { return m_sequential_dynamic_orderings; }
const std::vector<std::vector<DynamicPrintConfig>>& get_extruder_filament_info() const { return m_extruder_filament_info; }
void set_extruder_filament_info(const std::vector<std::vector<DynamicPrintConfig>>& filament_info) { m_extruder_filament_info = filament_info; }
@@ -1050,6 +1088,18 @@ public:
*/
std::vector<std::set<int>> get_physical_unprintable_filaments(const std::vector<unsigned int>& used_filaments) const;
/**
* @brief Determines the forbidden nozzle volume types for each used filament
*
* A filament may declare the extruder variants it supports. Every volume type offered by the
* printer's extruders that the filament does not support is forbidden for that filament.
* Hybrid volumes are ignored on both sides, and filaments declaring no variants are unrestricted.
*
* @param used_filaments Totally used filaments when slicing
* @return A map from used filament index to the set of nozzle volume types it cannot print on
*/
std::map<int, std::set<NozzleVolumeType>> get_filament_unprintable_flow(const std::vector<unsigned int> &used_filaments) const;
std::vector<double> get_extruder_printable_height() const;
std::vector<Polygons> get_extruder_printable_polygons() const;
std::vector<Polygons> get_extruder_unprintable_polygons() const;
@@ -1133,7 +1183,12 @@ public:
bool is_all_objects_are_short() const {
return std::all_of(this->objects().begin(), this->objects().end(), [&](PrintObject* obj) { return obj->height() < scale_(this->config().nozzle_height.value); });
}
// Post-slicing config-slot resolvers: map a (filament, layer) pair to the index of its
// per-(extruder x volume type) column in the expanded variant arrays, cached by grouping context.
int get_filament_config_indx(int filament_id, int layer_id);
int get_nozzle_config_index(int filament_id, int layer_id);
// Orca: Implement prusa's filament shrink compensation approach
// Returns if all used filaments have same shrinkage compensations.
bool has_same_shrinkage_compensations() const;
@@ -1143,6 +1198,57 @@ public:
std::tuple<float, float> object_skirt_offset(double margin_height = 0) const;
protected:
struct FilamentIndexKey
{
int filament_id;
ExtruderType extruder;
NozzleVolumeType nozzle_volume_type;
bool operator==(const FilamentIndexKey &other) const
{
return filament_id == other.filament_id && extruder == other.extruder && nozzle_volume_type == other.nozzle_volume_type;
}
};
struct PrintIndexKey
{
int filament_id;
int extruder_id;
ExtruderType extruder;
NozzleVolumeType nozzle_volume_type;
bool operator==(const PrintIndexKey &other) const
{
return filament_id == other.filament_id && extruder_id == other.extruder_id && extruder == other.extruder && nozzle_volume_type == other.nozzle_volume_type;
}
};
struct FilamentIndexKeyHash
{
std::size_t operator()(const FilamentIndexKey &k) const
{
size_t h1 = std::hash<int>{}(k.filament_id);
size_t h2 = std::hash<int>{}(static_cast<int>(k.extruder));
size_t h3 = std::hash<int>{}(static_cast<int>(k.nozzle_volume_type));
return h1 ^ (h2 << 8) ^ (h3 << 12);
}
};
struct PrintIndexKeyHash
{
std::size_t operator()(const PrintIndexKey &k) const
{
size_t h1 = std::hash<int>{}(k.filament_id);
size_t h2 = std::hash<int>{}(k.extruder_id);
size_t h3 = std::hash<int>{}(static_cast<int>(k.extruder));
size_t h4 = std::hash<int>{}(static_cast<int>(k.nozzle_volume_type));
return h1 ^ (h2 << 8) ^ (h3 << 12) ^ (h4 << 16);
}
};
using FilamentIndexMap = std::unordered_map<FilamentIndexKey, int, FilamentIndexKeyHash>;
using PrintIndexMap = std::unordered_map<PrintIndexKey, int, PrintIndexKeyHash>;
int get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, FilamentIndexMap &index_map);
int get_config_index(int filament_id, int layer_id, const std::vector<std::string> &variant_list, const std::vector<int>& self_index_list, PrintIndexMap &index_map);
// Invalidates the step, and its depending steps in Print.
bool invalidate_step(PrintStep step);
@@ -1156,6 +1262,16 @@ private:
void _make_skirt();
void _make_wipe_tower();
void finalize_first_layer_convex_hull();
void update_filament_self_index_cache();
// Deduplicates, per filament, the (extruder type x volume type) variants the grouping
// result routes it through; filaments the plan never routes get their default-map
// assignment so the slot resolution never depends on the (mutable) filament_map. config
// must carry extruder_type; returns false when it does not. Both the slice-time write-back
// and the apply-time reproduction call this with m_ori_full_print_config so the two
// expansions resolve identical slots.
bool collect_filament_variant_uses(const MultiNozzleUtils::LayeredNozzleGroupResult& group_result,
const DynamicPrintConfig& config,
std::unordered_map<int, std::vector<FilamentVariantUse>>& uses) const;
// Islands of objects and their supports extruded at the 1st layer.
Polygons first_layer_islands() const;
@@ -1174,7 +1290,7 @@ private:
PrintRegionPtrs m_print_regions;
//SoftFever
bool m_isBBLPrinter;
bool m_isBBLPrinter = false;
// Ordered collections of extrusion paths to build skirt loops and brim.
ExtrusionEntityCollection m_skirt;
@@ -1196,6 +1312,24 @@ private:
std::vector<std::vector<DynamicPrintConfig>> m_extruder_filament_info;
// Logical (extruder, nozzle) grouping result, set by ToolOrdering during reorder.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
// Sequential (by-object) selector plans, keyed by object; see sequential_dynamic_orderings().
// Rebuilt (or cleared) on every process().
std::map<const PrintObject*, ToolOrdering> m_sequential_dynamic_orderings;
// Used to cache filament parameter information
FilamentIndexMap m_filament_index_map;
// Used to cache printer and process parameter information
PrintIndexMap m_nozzle_index_map;
// Orca: filament ids already reported as missing a nozzle-group entry this slice. get_config_index()
// falls back per-filament/per-layer in the g-code hot path, so this dedupes its log to once per
// filament instead of flooding thousands of identical error lines. Cleared with the caches each slice.
std::set<int> m_missing_nozzle_group_logged;
// save the config value of "filament_self_index"
std::vector<int> m_filament_self_index;
// Following section will be consumed by the GCodeGenerator.
ToolOrdering m_tool_ordering;
WipeTowerData m_wipe_tower_data {m_tool_ordering};
+165 -32
View File
@@ -224,7 +224,11 @@ static t_config_option_keys print_config_diffs(
const DynamicPrintConfig &new_full_config,
DynamicPrintConfig &filament_overrides,
int plate_index,
std::vector<int>& filament_maps)
std::vector<int>& filament_maps,
// Per-slot machine indices when the filament arrays hold the per-variant expansion of a
// selector result (one slot per variant a filament migrates through); the per-filament
// map cannot index the expanded override arrays. Null on the single-slot path.
const std::vector<int>* dynamic_override_indices = nullptr)
{
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
@@ -240,7 +244,15 @@ static t_config_option_keys print_config_diffs(
const ConfigOption *opt_new_filament = std::binary_search(extruder_retract_keys.begin(), extruder_retract_keys.end(), opt_key) ? new_full_config.option(filament_prefix + opt_key) : nullptr;
if (opt_new_filament != nullptr) {
compute_filament_override_value(opt_key, opt_old, opt_new, opt_new_filament, new_full_config, print_diff, filament_overrides, filament_maps);
std::vector<int> filament_map_indices;
if (dynamic_override_indices)
filament_map_indices = *dynamic_override_indices;
else {
filament_map_indices.assign(filament_maps.size(), 0);
for (int i = 0; i < filament_maps.size(); i++)
filament_map_indices[i] = filament_maps[i] - 1;
}
compute_filament_override_value(opt_key, opt_old, opt_new, opt_new_filament, new_full_config, print_diff, filament_overrides, filament_map_indices);
} else if (*opt_new != *opt_old) {
//BBS: add plate_index logic for wipe_tower_x/wipe_tower_y
if (!opt_key.compare("wipe_tower_x") || !opt_key.compare("wipe_tower_y")) {
@@ -724,7 +736,7 @@ PrintObjectRegions::BoundingBox find_modifier_volume_extents(const PrintObjectRe
return out;
}
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders);
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders, std::vector<int>& variant_index);
void print_region_ref_inc(PrintRegion &r) { ++ r.m_ref_cnt; }
void print_region_ref_reset(PrintRegion &r) { r.m_ref_cnt = 0; }
@@ -738,7 +750,8 @@ bool verify_update_print_object_regions(
const PrintRegionConfig &default_region_config,
size_t num_extruders,
PrintObjectRegions &print_object_regions,
const std::function<void(const PrintRegionConfig&, const PrintRegionConfig&, const t_config_option_keys&)> &callback_invalidate)
const std::function<void(const PrintRegionConfig&, const PrintRegionConfig&, const t_config_option_keys&)> &callback_invalidate,
std::vector<int>& variant_index)
{
// Sort by ModelVolume ID.
model_volumes_sort_by_id(model_volumes);
@@ -783,7 +796,7 @@ bool verify_update_print_object_regions(
} else if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox))
// Such parent region does not exist. If it is needed, then we need to reslice.
// Only create new region for a modifier, which actually modifies config of it's parent.
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, **it_model_volume, num_extruders);
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, **it_model_volume, num_extruders, variant_index);
config != parent_region.region->config())
// This modifier newly overrides a region, which it did not before. We need to reslice.
return false;
@@ -791,8 +804,8 @@ bool verify_update_print_object_regions(
}
}
PrintRegionConfig cfg = region.parent == -1 ?
region_config_from_model_volume(default_region_config, layer_range.config, **it_model_volume, num_extruders) :
region_config_from_model_volume(layer_range.volume_regions[region.parent].region->config(), nullptr, **it_model_volume, num_extruders);
region_config_from_model_volume(default_region_config, layer_range.config, **it_model_volume, num_extruders, variant_index) :
region_config_from_model_volume(layer_range.volume_regions[region.parent].region->config(), nullptr, **it_model_volume, num_extruders, variant_index);
if (cfg != region.region->config()) {
// Region configuration changed.
if (print_region_ref_cnt(*region.region) == 0) {
@@ -964,6 +977,7 @@ static PrintObjectRegions* generate_print_object_regions(
size_t num_extruders,
const float xy_contour_compensation,
const std::vector<unsigned int> &painting_extruders,
std::vector<int> &variant_index,
const bool has_painted_fuzzy_skin)
{
// Reuse the old object or generate a new one.
@@ -1022,7 +1036,7 @@ static PrintObjectRegions* generate_print_object_regions(
// Add a model volume, assign an existing region or generate a new one.
layer_range.volume_regions.push_back({
&volume, -1,
get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders)),
get_create_region(region_config_from_model_volume(default_region_config, layer_range.config, volume, num_extruders, variant_index)),
bbox
});
} else if (volume.is_negative_volume()) {
@@ -1039,7 +1053,7 @@ static PrintObjectRegions* generate_print_object_regions(
if (parent_volume.is_model_part() || parent_volume.is_modifier())
if (PrintObjectRegions::BoundingBox parent_bbox = find_modifier_volume_extents(layer_range, parent_region_id); parent_bbox.intersects(*bbox)) {
// Only create new region for a modifier, which actually modifies config of it's parent.
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, volume, num_extruders);
if (PrintRegionConfig config = region_config_from_model_volume(parent_region.region->config(), nullptr, volume, num_extruders, variant_index);
config != parent_region.region->config()) {
added = true;
layer_range.volume_regions.push_back({ &volume, parent_region_id, get_create_region(std::move(config)), bbox });
@@ -1162,25 +1176,58 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
}
//apply extruder related values
std::vector<int> print_variant_index;
std::vector<std::vector<NozzleVolumeType>> nozzle_volume_types;
int extruder_count = 1, extruder_volume_type_count = 1;
bool different_extruder = false;
// Filled only when the filament arrays are rebuilt from a persisted selector result below;
// print_config_diffs then keys the retract overrides per expanded slot.
std::vector<int> dynamic_slot_indices;
different_extruder = new_full_config.support_different_extruders(extruder_count);
extruder_volume_type_count = new_full_config.get_extruder_nozzle_volume_count(extruder_count, nozzle_volume_types);
if (!extruder_applied) {
// variant_2 must be processed first, because variant_1 will make `printer_extruder_id` and `printer_extruder_variant` half of the size that makes `get_index_for_extruder` no longer work properly
new_full_config.update_values_to_printer_extruders(new_full_config, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
new_full_config.update_values_to_printer_extruders(new_full_config, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
//update print config related with variants
new_full_config.update_values_to_printer_extruders(new_full_config, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
if ((extruder_count > 1) || different_extruder) {
// variant_2 must be processed first, because variant_1 will make `printer_extruder_id` and `printer_extruder_variant` half of the size that makes `get_index_for_extruder` no longer work properly
new_full_config.update_values_to_printer_extruders(new_full_config, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_2, "printer_extruder_id", "printer_extruder_variant", 2);
new_full_config.update_values_to_printer_extruders(new_full_config, extruder_count, extruder_volume_type_count, nozzle_volume_types, printer_options_with_variant_1, "printer_extruder_id", "printer_extruder_variant");
//update print config related with variants
print_variant_index = new_full_config.update_values_to_printer_extruders(new_full_config, extruder_count, extruder_volume_type_count, nozzle_volume_types, print_options_with_variant, "print_extruder_id", "print_extruder_variant");
}
else
print_variant_index.resize(1, 0);
m_ori_full_print_config = new_full_config;
new_full_config.update_values_to_printer_extruders_for_multiple_filaments(new_full_config, filament_options_with_variant, "filament_self_index", "filament_extruder_variant");
std::set<std::string> filament_keys = filament_options_with_variant;
filament_keys.insert("filament_self_index");
// A persisted selector result with an actual migration means the last slice rebuilt the
// per-slot filament arrays from it (one slot per variant a filament prints through).
// Reproduce that exact expansion here so an unchanged config diffs empty — the expanded
// keys invalidate the wipe tower / g-code export, and the placeholder parser aliases
// the full config — instead of trimming back to one slot per filament.
auto group_result = std::dynamic_pointer_cast<MultiNozzleUtils::LayeredNozzleGroupResult>(this->get_nozzle_group_result());
std::unordered_map<int, std::vector<FilamentVariantUse>> filament_variant_uses;
if (group_result && group_result->is_support_dynamic_nozzle_map()
&& collect_filament_variant_uses(*group_result, m_ori_full_print_config, filament_variant_uses))
new_full_config.update_filament_config_values_for_multiple_extruders(m_ori_full_print_config, filament_variant_uses,
extruder_count, extruder_volume_type_count, filament_keys,
"filament_self_index", "filament_extruder_variant",
&dynamic_slot_indices);
else if ((extruder_count > 1) || different_extruder)
new_full_config.update_values_to_printer_extruders_for_multiple_filaments(m_ori_full_print_config, extruder_count, extruder_volume_type_count, filament_keys,
"filament_self_index", "filament_extruder_variant");
}
else {
//should not come here, we can not get the result of print_variant, for the values have been updated
//we just use the default values here
auto variant_opt = dynamic_cast<const ConfigOptionStrings *>(new_full_config.option("printer_extruder_variant"));
print_variant_index.resize(variant_opt->values.size());
for (int e_index = 0; e_index < variant_opt->values.size(); e_index++)
{
print_variant_index[e_index] = e_index;
}
}
// else {
// int extruder_count;
// bool different_extruder = new_full_config.support_different_extruders(extruder_count);
// print_variant_index.resize(extruder_count);
// for (int e_index = 0; e_index < extruder_count; e_index++)
// {
// print_variant_index[e_index] = e_index;
// }
// }
auto opt_filament_map = new_full_config.option<ConfigOptionInts>("filament_map");
std::vector<int> filament_maps = opt_filament_map ? opt_filament_map->values : std::vector<int>();
@@ -1188,7 +1235,15 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Find modified keys of the various configs. Resolve overrides extruder retract values by filament profiles.
DynamicPrintConfig filament_overrides;
//BBS: add plate index
t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index, filament_maps);
t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index, filament_maps,
dynamic_slot_indices.empty() ? nullptr : &dynamic_slot_indices);
// Orca: filament_map_2 is engine-derived state, never a user input: the rebuild below
// recomputes it from filament_map/filament_volume_map/the variant slots on every apply
// (all of which are diffed and invalidation-listed on their own), and the grouping
// write-back overwrites it during process(). The incoming full config only ever carries
// the ConfigDef default, so diffing it would invalidate every print step on each apply
// for any multi-extruder printer and permanently invalidate fresh slice results.
print_diff.erase(std::remove(print_diff.begin(), print_diff.end(), "filament_map_2"), print_diff.end());
t_config_option_keys full_config_diff = full_print_config_diffs(m_full_print_config, new_full_config, this->m_plate_index);
// Collect changes to object and region configs.
t_config_option_keys object_diff = m_default_object_config.diff(new_full_config);
@@ -1196,10 +1251,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
//BBS: process the filament_map related logic
std::unordered_set<std::string> print_diff_set(print_diff.begin(), print_diff.end());
if (print_diff_set.find("filament_map_mode") == print_diff_set.end())
if (!print_diff_set.empty() && print_diff_set.find("filament_map_mode") == print_diff_set.end())
{
FilamentMapMode map_mode = new_full_config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value;
if (map_mode < fmmManual) {
if (is_auto_filament_map_mode(map_mode)) {
if (print_diff_set.find("filament_map") != print_diff_set.end()) {
print_diff_set.erase("filament_map");
//full_config_diff.erase("filament_map");
@@ -1208,9 +1263,29 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
old_opt->set(new_opt);
m_config.filament_map = *new_opt;
}
if (print_diff_set.find("filament_volume_map") != print_diff_set.end()) {
print_diff_set.erase("filament_volume_map");
//full_config_diff.erase("filament_volume_map");
ConfigOptionInts* old_opt = m_full_print_config.option<ConfigOptionInts>("filament_volume_map", true);
ConfigOptionInts* new_opt = new_full_config.option<ConfigOptionInts>("filament_volume_map", true);
old_opt->set(new_opt);
m_config.filament_volume_map = *new_opt;
}
if (print_diff_set.find("filament_nozzle_map") != print_diff_set.end()) {
print_diff_set.erase("filament_nozzle_map");
//full_config_diff.erase("filament_nozzle_map");
ConfigOptionInts* old_opt = m_full_print_config.option<ConfigOptionInts>("filament_nozzle_map", true);
ConfigOptionInts* new_opt = new_full_config.option<ConfigOptionInts>("filament_nozzle_map", true);
old_opt->set(new_opt);
m_config.filament_nozzle_map = *new_opt;
}
}
else {
print_diff_set.erase("extruder_ams_count");
if (map_mode == fmmManual) {
// filament_nozzle_map is an engine output, not a GUI input, in manual mode
print_diff_set.erase("filament_nozzle_map");
}
std::vector<int> old_filament_map = m_config.filament_map.values;
std::vector<int> new_filament_map = new_full_config.option<ConfigOptionInts>("filament_map", true)->values;
@@ -1227,14 +1302,62 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
break;
}
}
if (same_map)
if (same_map) {
print_diff_set.erase("filament_map");
// The extruder retract overrides are keyed by the (unchanged) filament map;
// recompute them and drop diffs whose recomputed value matches the current
// config, so a cosmetic reordering of unused filaments does not invalidate.
const auto& retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
std::vector<int> old_f_map_indices(old_filament_map.size(), 0);
for (size_t i = 0; i < old_filament_map.size(); i++)
old_f_map_indices[i] = old_filament_map[i] - 1;
for (const auto& rk : retract_keys) {
if (print_diff_set.find(rk) == print_diff_set.end())
continue;
const ConfigOption* opt_old = m_config.option(rk);
const ConfigOption* opt_new_m = new_full_config.option(rk);
const ConfigOption* opt_new_f = new_full_config.option(filament_prefix + rk);
if (opt_old && opt_new_m && opt_new_f) {
std::unique_ptr<ConfigOption> opt_recomputed(opt_new_m->clone());
opt_recomputed->apply_override(opt_new_f, old_f_map_indices);
if (*opt_old == *opt_recomputed)
print_diff_set.erase(rk);
}
}
}
}
}
if (print_diff_set.size() != print_diff.size())
print_diff.assign(print_diff_set.begin(), print_diff_set.end());
}
//filament_map_2
// Orca: seed with 0-based extruder indices so the copy stays a valid slot map even when the
// variant options are absent below and the rebuild loop is skipped (unit tests, degenerate
// presets); the loop overwrites every entry when it runs.
m_config.filament_map_2.values = filament_maps;
for (auto& v : m_config.filament_map_2.values)
--v;
auto opt_extruder_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(new_full_config.option("extruder_type"));
auto opt_filament_volume_maps = dynamic_cast<const ConfigOptionInts*>(new_full_config.option("filament_volume_map"));
auto opt_nozzle_volume_type = dynamic_cast<const ConfigOptionEnumsGeneric*>(new_full_config.option("nozzle_volume_type"));
for (int index = 0; opt_extruder_type && opt_nozzle_volume_type && index < filament_maps.size(); index++)
{
ExtruderType extruder_type = (ExtruderType)(opt_extruder_type->get_at(filament_maps[index] - 1));
NozzleVolumeType nozzle_volume_type = (NozzleVolumeType)(opt_nozzle_volume_type->get_at(filament_maps[index] - 1));
// Orca: honour the per-filament volume map only when a producer sized it to the filament
// count; mis-sized maps (stale project values, CLI runs until the per-filament synthesis
// lands there) must not be indexed per filament (see
// update_values_to_printer_extruders_for_multiple_filaments for the same guard).
if ((extruder_volume_type_count > extruder_count) && opt_filament_volume_maps
&& opt_filament_volume_maps->values.size() == filament_maps.size())
nozzle_volume_type = (NozzleVolumeType)(opt_filament_volume_maps->values[index]);
m_config.filament_map_2.values[index] = new_full_config.get_index_for_extruder(filament_maps[index], "print_extruder_id", extruder_type, nozzle_volume_type, "print_extruder_variant");
}
// Do not use the ApplyStatus as we will use the max function when updating apply_status.
unsigned int apply_status = APPLY_STATUS_UNCHANGED;
auto update_apply_status = [&apply_status](bool invalidated)
@@ -1282,6 +1405,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_region_config.apply_only(new_full_config, region_diff, true);
//m_full_print_config = std::move(new_full_config);
m_full_print_config = new_full_config;
update_filament_self_index_cache();
if (num_extruders != m_config.filament_diameter.size()) {
num_extruders = m_config.filament_diameter.size();
num_extruders_changed = true;
@@ -1475,7 +1599,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
if (object_config_changed)
model_object.config.assign_config(model_object_new.config);
if (! object_diff.empty() || object_config_changed || num_extruders_changed ) {
PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_extruders );
PrintObjectConfig new_config = PrintObject::object_config_from_model_object(m_default_object_config, model_object, num_extruders, print_variant_index);
for (const PrintObjectStatus &print_object_status : print_object_status_db.get_range(model_object)) {
t_config_option_keys diff = print_object_status.print_object->config().diff(new_config);
if (! diff.empty()) {
@@ -1541,10 +1665,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Generate a list of trafos and XY offsets for instances of a ModelObject
// Producing the config for PrintObject on demand, caching it at print_object_last.
const PrintObject *print_object_last = nullptr;
auto print_object_apply_config = [this, &print_object_last, model_object, num_extruders ](PrintObject *print_object) {
auto print_object_apply_config = [this, &print_object_last, model_object, num_extruders, &print_variant_index](PrintObject *print_object) {
print_object->config_apply(print_object_last ?
print_object_last->config() :
PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders ));
PrintObject::object_config_from_model_object(m_default_object_config, *model_object, num_extruders, print_variant_index));
print_object_last = print_object;
};
if (old.empty()) {
@@ -1654,7 +1778,14 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_default_object_config.apply_only(new_full_config, new_changed_keys, true);
// Handle changes to regions config defaults
m_default_region_config.apply_only(new_full_config, new_changed_keys, true);
// Orca: keep the pre-expansion snapshot in sync with this late normalization pass.
// The engine map write-back rebuilds m_full_print_config from m_ori_full_print_config
// after slicing; a stale snapshot would resurrect the un-normalized values (e.g.
// enable_prime_tower on a single-filament print) in the dumped config and spuriously
// re-invalidate the g-code on the next apply.
m_ori_full_print_config.apply_only(new_full_config, new_changed_keys, true);
m_full_print_config = std::move(new_full_config);
update_filament_self_index_cache();
}
// All regions now have distinct settings.
@@ -1715,7 +1846,8 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
for (auto it = it_print_object; it != it_print_object_end; ++it)
if ((*it)->m_shared_regions != nullptr)
update_apply_status((*it)->invalidate_state_by_config_options(old_config, new_config, diff_keys));
})) {
},
print_variant_index)) {
// Regions are valid, just keep them.
} else {
// Regions were reshuffled.
@@ -1737,6 +1869,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
num_extruders ,
print_object.is_mm_painted() ? 0.f : float(print_object.config().xy_contour_compensation.value),
painting_extruders,
print_variant_index,
print_object.is_fuzzy_skin_painted());
}
for (auto it = it_print_object; it != it_print_object_end; ++it)
File diff suppressed because it is too large Load Diff
+213 -9
View File
@@ -46,6 +46,14 @@ enum GCodeFlavor : unsigned char {
gcfNoExtrusion
};
// How a filament is used across the model. Part of the multi-nozzle grouping data; not yet
// read by the shipping slicer — the nozzle-centric FilamentGroup engine consumes it.
enum FilamentUsageType {
SupportOnly,
ModelOnly,
Hybrid
};
enum class FuzzySkinType {
None,
@@ -62,6 +70,19 @@ enum class FuzzySkinMode {
Combined,
};
// ORCA: direction in which top_surface_expansion grows the top surfaces.
enum class TopSurfaceExpansionDirection {
InwardAndOutward,
Inward,
Outward,
};
enum class CenterOfSurfacePattern {
Each_Surface,
Each_Model,
Each_Assembly,
};
enum class NoiseType {
Classic,
Perlin,
@@ -97,6 +118,34 @@ enum InfillPattern : int {
ipCount,
};
// Orca: Infill patterns whose alignment origin follows the fill bounding box, so the
// "separated_infills" option can re-center them per connected body. Patterns evaluated in
// absolute/global coordinates (Gyroid, TPMS, Honeycomb, CrossHatch, ...) or that are shape-relative
// (Concentric) ignore that bounding box and are therefore excluded.
inline bool is_separable_infill_pattern(InfillPattern pattern)
{
switch (pattern) {
case ipRectilinear:
case ipAlignedRectilinear:
case ipZigZag:
case ipCrossZag:
case ipLockedZag:
case ipGrid:
case ipTriangles:
case ipStars: // tri-hexagon
case ipCubic:
case ipQuarterCubic:
case ipLateralHoneycomb:
case ipLateralLattice:
case ipHilbertCurve:
case ipArchimedeanChords:
case ipOctagramSpiral:
return true;
default:
return false;
}
}
enum class IroningType {
NoIroning,
TopSurfaces,
@@ -144,6 +193,15 @@ enum class WallDirection
Count,
};
// Orca: print order of surface fill loops/fragments for center-based fill patterns
// (Concentric, Archimedean Chords, Octagram Spiral).
enum class SurfaceFillOrder {
Default,
Outward,
Inward,
Count,
};
//BBS
enum class PrintSequence {
ByLayer,
@@ -300,6 +358,12 @@ enum class PerimeterGeneratorType
Arachne
};
enum class ToolChangeOrderingType
{
Default,
Cyclic,
};
// BBS
enum OverhangFanThreshold {
Overhang_threshold_none = 0,
@@ -335,6 +399,13 @@ enum LayerSeq {
flsCustomize
};
enum FanDirection {
fdUndefine = 0,
fdLeft,
fdRight,
fdBoth
};
static std::unordered_map<NozzleType, std::string>NozzleTypeEumnToStr = {
{NozzleType::ntUndefine, "undefine"},
{NozzleType::ntHardenedSteel, "hardened_steel"},
@@ -418,24 +489,49 @@ enum ExtruderType {
enum NozzleVolumeType {
nvtStandard = 0,
nvtHighFlow,
nvtMaxNozzleVolumeType = nvtHighFlow
nvtHybrid, // extruder holds a mix of Standard and High Flow sub-nozzles; selectable only for extruders
// with more than one sub-nozzle (extruder_max_nozzle_count > 1); matched as Standard for
// preset lookup and never emitted in profile variant strings
nvtTPUHighFlow, // physical variant, used on H2D/H2DP 0.4 nozzles only
// Integer values are serialized as raw ints in 3mf plate metadata and device MQTT, so they MUST stay stable.
nvtMaxNozzleVolumeType = nvtTPUHighFlow
};
enum FilamentMapMode {
fmmAutoForFlush,
fmmAutoForMatch,
fmmManual,
fmmNozzleManual, // Fully-manual filament->physical-nozzle mapping (filament_nozzle_map). Kept ordered right after fmmManual so every `< fmmManual` "is-auto" check stays correct.
fmmDefault
};
// All auto modes are ordered before fmmManual (see the enum ordering note above).
inline bool is_auto_filament_map_mode(FilamentMapMode mode) {
return mode < fmmManual;
}
// Dual-extruder purge control. Default reproduces the current
// per-extruder flush_multiplier + filament_prime_volume behaviour, so absent/default is inert.
// Saving -> reduce prime volume to 15 mm3; Fast -> use flush_multiplier_fast + filament_flush_temp_fast.
enum PrimeVolumeMode {
pvmDefault = 0,
pvmSaving,
pvmFast
};
extern std::string get_extruder_variant_string(ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type);
// Base slot lookup: scans a variant list (paired with its 1-based extruder/filament ids) for the
// entry matching the given extruder/volume type and id. Returns 0 when no entry matches.
extern int get_config_index_base(NozzleVolumeType volume_type, ExtruderType extruder_type, int variant_id_1based, const std::vector<std::string>& variant_list, const std::vector<int>& variant_ids_1based);
static std::set<NozzleVolumeType> get_valid_nozzle_volume_type() {
std::set<NozzleVolumeType> type;
for (int i = 0; i <= nvtMaxNozzleVolumeType; ++i) {
auto t = static_cast<NozzleVolumeType>(i);
// TODO: Orca: Support hybrid
//if (t == nvtHybrid) continue;
// Hybrid is not a physical nozzle variant: presets never define it, so it must not
// produce a variant string.
if (t == nvtHybrid) continue;
type.insert(t);
}
return type;
@@ -521,11 +617,20 @@ static std::string get_bed_temp_1st_layer_key(const BedType type)
}
extern const std::vector<std::string> filament_extruder_override_keys;
// Full override-key check incl. filament_retract_length_nc (defined outside the generator list).
extern bool is_filament_extruder_override_key(const std::string &opt_key);
// for parse extruder_ams_count
extern std::vector<std::map<int, int>> get_extruder_ams_count(const std::vector<std::string> &strs);
extern std::vector<std::string> save_extruder_ams_count_to_string(const std::vector<std::map<int, int>> &extruder_ams_count);
// maps a full extruder variant string (e.g. "Direct Drive High Flow") to its NozzleVolumeType; nvtHybrid if unparsable
extern NozzleVolumeType convert_to_nvt_type(const std::string& variant_str);
// for parse extruder_nozzle_stats (per-extruder physical nozzle inventory by volume type)
extern std::vector<std::map<NozzleVolumeType, int>> get_extruder_nozzle_stats(const std::vector<std::string> &strs);
extern std::vector<std::string> save_extruder_nozzle_stats_to_string(const std::vector<std::map<NozzleVolumeType, int>> &extruder_nozzle_stats);
#define CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NAME) \
template<> const t_config_enum_names& ConfigOptionEnum<NAME>::get_enum_names(); \
template<> const t_config_enum_values& ConfigOptionEnum<NAME>::get_enum_values();
@@ -534,6 +639,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrinterTechnology)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(GCodeFlavor)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FuzzySkinMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TopSurfaceExpansionDirection)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(NoiseType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(InfillPattern)
@@ -561,7 +667,9 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PrintHostType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AuthorizationType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(WipeTowerWallType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -613,6 +721,23 @@ class StaticPrintConfig;
// Minimum object distance for arrangement, based on printer technology.
double min_object_distance(const ConfigBase &cfg);
// One (extruder type x nozzle volume type) parameter variant a filament prints through, plus a
// representative physical extruder observed using it. Ordering (and set-dedup identity) covers
// the variant pair only, so the same variant reached through two extruders keeps one config slot.
struct FilamentVariantUse
{
ExtruderType extruder_type{etDirectDrive};
NozzleVolumeType nozzle_volume_type{nvtStandard};
int extruder_id{0}; // 0-based, first extruder seen using this variant
bool operator<(const FilamentVariantUse &other) const
{
if (extruder_type != other.extruder_type)
return extruder_type < other.extruder_type;
return nozzle_volume_type < other.nozzle_volume_type;
}
};
// Slic3r dynamic configuration, used to override the configuration
// per object, per modification volume or per printing material.
// The dynamic configuration is also used to store user modifications of the print global parameters,
@@ -670,9 +795,26 @@ public:
//BBS
bool is_using_different_extruders();
bool support_different_extruders(int& extruder_count) const;
// Counts the config slots of a printer: one per (extruder x nozzle volume type) as described by
// extruder_nozzle_stats, or simply one per extruder when the stats are absent/mismatched.
// Fills nozzle_volume_types with each extruder's volume types in ascending enum order.
int get_extruder_nozzle_volume_count(int extruder_count, std::vector<std::vector<NozzleVolumeType>>& nozzle_volume_types) const;
int get_index_for_extruder(int extruder_or_filament_id, std::string id_name, ExtruderType extruder_type, NozzleVolumeType nozzle_volume_type, std::string variant_name, unsigned int stride = 1) const;
void update_values_to_printer_extruders(DynamicPrintConfig& printer_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name, unsigned int stride = 1, unsigned int extruder_id = 0);
void update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, std::set<std::string>& key_set, std::string id_name, std::string variant_name);
std::vector<int> update_values_to_printer_extruders(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::vector<std::vector<NozzleVolumeType>>& nv_types,
std::set<std::string>& key_set, std::string id_name, std::string variant_name, unsigned int stride = 1, unsigned int extruder_id = 0, NozzleVolumeType filament_nvt = nvtStandard);
void update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set<std::string>& key_set, std::string id_name, std::string variant_name);
// Rebuilds the per-slot filament arrays from a per-layer grouping outcome: a filament that
// prints through several (extruder x nozzle volume type) variants keeps one slot per variant
// (unlike the single-slot rebuild above), so layer-aware consumers can resolve the slot the
// current layer actually prints with. Filaments absent from filament_variant_uses keep a
// single slot resolved from filament_map / filament_volume_map. When slot_machine_indices is
// non-null it receives one machine-variant slot index per output slot (the nil-value fallback
// keying for the extruder retract overrides; a per-filament map cannot index expanded arrays).
void update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config,
const std::unordered_map<int, std::vector<FilamentVariantUse>>& filament_variant_uses,
int extruder_count, int extruder_nozzle_volume_count,
std::set<std::string>& key_set, std::string id_name, std::string variant_name,
std::vector<int>* slot_machine_indices = nullptr);
void update_non_diff_values_to_base_config(DynamicPrintConfig& new_config, const t_config_option_keys& keys, const std::set<std::string>& different_keys, std::string extruder_id_name, std::string extruder_variant_name,
std::set<std::string>& key_set1, std::set<std::string>& key_set2);
@@ -698,8 +840,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;
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);
extern void compute_filament_override_value(const std::string& opt_key, const ConfigOption *opt_old_machine, const ConfigOption *opt_new_machine, const ConfigOption *opt_new_filament, const DynamicPrintConfig& new_full_config,
t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector<int>& f_maps);
t_config_option_keys& diff_keys, DynamicPrintConfig& filament_overrides, std::vector<int>& f_map_indices);
void handle_legacy_sla(DynamicPrintConfig &config);
@@ -1076,9 +1219,6 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionInt, interlocking_beam_layer_count))
((ConfigOptionInt, interlocking_depth))
((ConfigOptionInt, interlocking_boundary_avoidance))
// Orca: internal use only
((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration
)
// This object is mapped to Perl as Slic3r::Config::PrintRegion.
@@ -1102,11 +1242,15 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionPercent, bottom_surface_density))
((ConfigOptionEnum<InfillPattern>, top_surface_pattern))
((ConfigOptionEnum<InfillPattern>, bottom_surface_pattern))
((ConfigOptionEnum<SurfaceFillOrder>, top_surface_fill_order))
((ConfigOptionEnum<SurfaceFillOrder>, bottom_surface_fill_order))
((ConfigOptionEnum<InfillPattern>, internal_solid_infill_pattern))
((ConfigOptionFloatOrPercent, outer_wall_line_width))
((ConfigOptionFloatsNullable, outer_wall_speed))
((ConfigOptionFloat, infill_direction))
((ConfigOptionFloat, solid_infill_direction))
((ConfigOptionFloat, top_layer_direction))
((ConfigOptionFloat, bottom_layer_direction))
((ConfigOptionString, solid_infill_rotate_template))
((ConfigOptionBool, symmetric_infill_y_axis))
((ConfigOptionFloat, infill_shift_step))
@@ -1120,6 +1264,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, lightning_prune_angle))
((ConfigOptionFloat, lightning_straightening_angle))
((ConfigOptionBool, align_infill_direction_to_model))
((ConfigOptionEnum<CenterOfSurfacePattern>, center_of_surface_pattern))
((ConfigOptionBool, separated_infills))
((ConfigOptionString, extra_solid_infills))
((ConfigOptionEnum<FuzzySkinType>, fuzzy_skin))
((ConfigOptionFloat, fuzzy_skin_thickness))
@@ -1185,6 +1331,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloatOrPercent, top_surface_line_width))
((ConfigOptionInt, top_shell_layers))
((ConfigOptionFloat, top_shell_thickness))
((ConfigOptionFloat, top_surface_expansion))
((ConfigOptionFloat, top_surface_expansion_margin))
((ConfigOptionEnum<TopSurfaceExpansionDirection>, top_surface_expansion_direction))
((ConfigOptionFloatsNullable, top_surface_speed))
//BBS
((ConfigOptionBoolsNullable, enable_overhang_speed))
@@ -1209,6 +1358,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloat, filter_out_gap_fill))
((ConfigOptionFloatsOrPercentsNullable, small_perimeter_speed))
((ConfigOptionFloatsNullable, small_perimeter_threshold))
((ConfigOptionFloatsOrPercentsNullable, small_support_perimeter_speed))
((ConfigOptionFloatsNullable, small_support_perimeter_threshold))
((ConfigOptionFloat, top_solid_infill_flow_ratio))
((ConfigOptionFloat, bottom_solid_infill_flow_ratio))
((ConfigOptionFloatOrPercent, infill_anchor))
@@ -1221,6 +1372,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, hole_to_polyhole))
((ConfigOptionFloatOrPercent, hole_to_polyhole_threshold))
((ConfigOptionBool, hole_to_polyhole_twisted))
((ConfigOptionInt, hole_to_polyhole_max_edges))
((ConfigOptionBool, overhang_reverse))
((ConfigOptionBool, overhang_reverse_internal_only))
((ConfigOptionFloatOrPercent, overhang_reverse_threshold))
@@ -1291,6 +1444,12 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloats, machine_min_travel_rate))
// M205 S... [mm/sec]
((ConfigOptionFloats, machine_min_extruding_rate))
// Bedslinger mass/force model: drive the per-layer Y-axis
// acceleration limit (curr_y_acceleration_limit) and the printed-mass check.
// Default 0 => inactive for every existing printer (mass model reads them as disabled).
((ConfigOptionFloat, machine_max_force_Y))
((ConfigOptionFloat, machine_bed_mass_Y))
((ConfigOptionFloat, machine_max_printed_mass))
//resonance avoidance ported from qidi slicer
((ConfigOptionBool, resonance_avoidance))
@@ -1345,6 +1504,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, filament_vendor))
((ConfigOptionBools, filament_is_support))
((ConfigOptionInts, filament_printable))
((ConfigOptionInts, filament_extruder_compatibility))
((ConfigOptionFloats, filament_change_length))
((ConfigOptionFloats, filament_cost))
((ConfigOptionStrings, default_filament_colour))
@@ -1353,14 +1513,20 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionInts, required_nozzle_HRC))
((ConfigOptionEnum<FilamentMapMode>, filament_map_mode))
((ConfigOptionInts, filament_map))
((ConfigOptionInts, filament_volume_map))
((ConfigOptionInts, filament_nozzle_map))
((ConfigOptionInts, filament_map_2)) //used for multi nozzle, map filament to the index identified by extruder+nozzle_volume_type
//((ConfigOptionInts, filament_extruder_id))
((ConfigOptionStrings, filament_extruder_variant))
((ConfigOptionInts, filament_self_index))
((ConfigOptionBool, support_object_skip_flush))
((ConfigOptionEnum<BedTempFormula>, bed_temperature_formula))
((ConfigOptionInts, physical_extruder_map))
((ConfigOptionIntsNullable, nozzle_flush_dataset))
((ConfigOptionFloatsNullable, filament_flush_volumetric_speed))
((ConfigOptionIntsNullable, filament_flush_temp))
// Fast-purge flush temperature; consumed only when prime_volume_mode==pvmFast.
((ConfigOptionIntsNullable, filament_flush_temp_fast))
// BBS
((ConfigOptionBool, scan_first_layer))
((ConfigOptionEnum<PowerLossRecoveryMode>, enable_power_loss_recovery))
@@ -1384,6 +1550,9 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionPercents, retract_before_wipe))
// Orca
((ConfigOptionPercents, retract_after_wipe))
((ConfigOptionFloats, retraction_length))
((ConfigOptionFloats, retract_length_toolchange))
((ConfigOptionInt, enable_long_retraction_when_cut))
@@ -1407,6 +1576,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, single_extruder_multi_material))
((ConfigOptionBool, manual_filament_change))
((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode))
@@ -1421,12 +1591,16 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionEnumsGenericNullable,nozzle_type))
((ConfigOptionInt, nozzle_hrc))
((ConfigOptionBool, auxiliary_fan))
((ConfigOptionEnum<FanDirection>, fan_direction))
((ConfigOptionBool, support_air_filtration))
((ConfigOptionBool, support_cooling_filter))
((ConfigOptionBool, cooling_filter_enabled))
((ConfigOptionEnum<PrinterStructure>,printer_structure))
((ConfigOptionBool, support_chamber_temp_control))
((ConfigOptionEnumsGeneric, extruder_type))
((ConfigOptionEnumsGeneric, nozzle_volume_type))
((ConfigOptionStrings, extruder_ams_count))
((ConfigOptionStrings, extruder_nozzle_stats))
((ConfigOptionInts, printer_extruder_id))
((ConfigOptionInt, master_extruder_id))
((ConfigOptionStrings, printer_extruder_variant))
@@ -1484,6 +1658,26 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, small_area_infill_flow_compensation_model))
((ConfigOptionBool, has_scarf_joint_seam))
// Multi-nozzle + pre-heating + nozzle-change (nc) keys. Defaults are no-ops for existing
// single-nozzle printers; new slicing paths gate on extruder_max_nozzle_count > 1.
((ConfigOptionFloat, machine_hotend_change_time))
((ConfigOptionFloat, machine_prepare_compensation_time))
((ConfigOptionBool, enable_pre_heating))
((ConfigOptionFloatsNullable, hotend_cooling_rate))
((ConfigOptionFloatsNullable, hotend_heating_rate))
((ConfigOptionFloats, filament_change_length_nc))
((ConfigOptionFloatsNullable, filament_ramming_travel_time))
((ConfigOptionIntsNullable, filament_pre_cooling_temperature))
((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed))
((ConfigOptionFloatsNullable, filament_ramming_travel_time_nc))
((ConfigOptionIntsNullable, filament_pre_cooling_temperature_nc))
((ConfigOptionFloatsNullable, filament_ramming_volumetric_speed_nc))
((ConfigOptionFloatsNullable, filament_retract_length_nc))
((ConfigOptionIntsNullable, extruder_max_nozzle_count))
// Printer flag: whether the printer offers the fast-purge mode selector.
// Default false; no shipping profile sets it, so the fast-purge UI stays hidden.
((ConfigOptionBool, support_fast_purge_mode))
)
// This object is mapped to Perl as Slic3r::Config::Print.
@@ -1631,7 +1825,15 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
// BBS: wipe tower is only used for priming
((ConfigOptionFloat, prime_volume))
// Nozzle-change (nc) prime volume + pre-heat delta
((ConfigOptionFloats, filament_prime_volume_nc))
((ConfigOptionFloatsNullable, filament_preheat_temperature_delta))
((ConfigOptionFloats, flush_multiplier))
// Fast-purge mode. Kept out of the g-code config block (banned_keys in
// GCode::append_full_config) so registering them leaves the shipping fleet's g-code byte-identical;
// consumed only on the prime_volume_mode==pvmFast / pvmSaving branch (default pvmDefault = inert).
((ConfigOptionEnum<PrimeVolumeMode>, prime_volume_mode))
((ConfigOptionFloats, flush_multiplier_fast))
((ConfigOptionFloat, z_offset))
// BBS: project filaments
((ConfigOptionFloats, filament_colour_new))
@@ -1639,6 +1841,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloatsNullable, nozzle_volume))
((ConfigOptionPoints, start_end_points))
((ConfigOptionEnum<TimelapseType>, timelapse_type))
// Corexy farthest-point timelapse (default false → inert for existing printers)
((ConfigOptionBool, farthest_point_timelapse))
((ConfigOptionString, thumbnails))
// BBS: move from PrintObjectConfig
((ConfigOptionBool, independent_support_layer_height))
+184 -29
View File
@@ -4,6 +4,7 @@
#include "Print.hpp"
#include "BoundingBox.hpp"
#include "ClipperUtils.hpp"
#include "Clipper2Utils.hpp"
#include "ElephantFootCompensation.hpp"
#include "Geometry.hpp"
#include "I18N.hpp"
@@ -97,7 +98,7 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Transfor
// snug height and an approximate bounding box in XY.
BoundingBoxf3 bbox = model_object->raw_bounding_box();
Vec3d bbox_center = bbox.center();
// We may need to rotate the bbox / bbox_center from the original instance to the current instance.
double z_diff = Geometry::rotation_diff_z(model_object->instances.front()->get_rotation(), instances.front().model_instance->get_rotation());
if (std::abs(z_diff) > EPSILON) {
@@ -157,10 +158,10 @@ std::vector<std::reference_wrapper<const PrintRegion>> PrintObject::all_regions(
return out;
}
Polygons create_polyholes(const Point center, const coord_t radius, const coord_t nozzle_diameter, bool multiple)
Polygons create_polyholes(const Point center, const coord_t radius, const coord_t nozzle_diameter, bool multiple, int max_edges)
{
// n = max(round(2 * d), 3); // for 0.4mm nozzle
size_t nb_edges = (int)std::max(3, (int)std::round(4.0 * unscaled(radius) * 0.4 / unscaled(nozzle_diameter)));
size_t nb_edges = (int)std::min(max_edges, std::max(3, (int)std::round(4.0 * unscaled(radius) * 0.4 / unscaled(nozzle_diameter))));
// cylinder(h = h, r = d / cos (180 / n), $fn = n);
//create x polyholes by rotation if multiple
int nb_polyhole = 1;
@@ -190,8 +191,8 @@ void PrintObject::_transform_hole_to_polyholes()
{
// get all circular holes for each layer
// the id is center-diameter-extruderid
//the tuple is Point center; float diameter_max; int extruder_id; coord_t max_variation; bool twist;
std::vector<std::vector<std::pair<std::tuple<Point, float, int, coord_t, bool>, Polygon*>>> layerid2center;
//the tuple is Point center; float diameter_max; int extruder_id; coord_t max_variation; bool twist; int max_edges;
std::vector<std::vector<std::pair<std::tuple<Point, float, int, coord_t, bool, int>, Polygon*>>> layerid2center;
for (size_t i = 0; i < this->m_layers.size(); i++) layerid2center.emplace_back();
tbb::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()),
@@ -230,9 +231,10 @@ void PrintObject::_transform_hole_to_polyholes()
// SCALED_EPSILON was a bit too harsh. Now using a config, as some may want some harsh setting and some don't.
coord_t max_variation = std::max(SCALED_EPSILON, scale_(this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_threshold.get_abs_value(unscaled(diameter_sum / hole.points.size()))));
bool twist = this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_twisted.value;
int max_edges = this->m_layers[layer_idx]->m_regions[region_idx]->region().config().hole_to_polyhole_max_edges.value;
if (diameter_max - diameter_min < max_variation * 2 && diameter_line_max - diameter_line_min < max_variation * 2) {
layerid2center[layer_idx].emplace_back(
std::tuple<Point, float, int, coord_t, bool>{center, diameter_max, layer->m_regions[region_idx]->region().config().outer_wall_filament_id.value, max_variation, twist}, & hole);
std::tuple<Point, float, int, coord_t, bool, int>{center, diameter_max, layer->m_regions[region_idx]->region().config().outer_wall_filament_id.value, max_variation, twist, max_edges}, & hole);
}
}
}
@@ -243,14 +245,14 @@ void PrintObject::_transform_hole_to_polyholes()
}
});
//sort holes per center-diameter
std::map<std::tuple<Point, float, int, coord_t, bool>, std::vector<std::pair<Polygon*, int>>> id2layerz2hole;
std::map<std::tuple<Point, float, int, coord_t, bool, int>, std::vector<std::pair<Polygon*, int>>> id2layerz2hole;
//search & find hole that span at least X layers
const size_t min_nb_layers = 2;
for (size_t layer_idx = 0; layer_idx < this->m_layers.size(); ++layer_idx) {
for (size_t hole_idx = 0; hole_idx < layerid2center[layer_idx].size(); ++hole_idx) {
//get all other same polygons
std::tuple<Point, float, int, coord_t, bool>& id = layerid2center[layer_idx][hole_idx].first;
std::tuple<Point, float, int, coord_t, bool, int>& id = layerid2center[layer_idx][hole_idx].first;
float max_z = layers()[layer_idx]->print_z;
std::vector<std::pair<Polygon*, int>> holes;
holes.emplace_back(layerid2center[layer_idx][hole_idx].second, layer_idx);
@@ -258,7 +260,7 @@ void PrintObject::_transform_hole_to_polyholes()
if (layers()[search_layer_idx]->print_z - layers()[search_layer_idx]->height - max_z > EPSILON) break;
//search an other polygon with same id
for (size_t search_hole_idx = 0; search_hole_idx < layerid2center[search_layer_idx].size(); ++search_hole_idx) {
std::tuple<Point, float, int, coord_t, bool>& search_id = layerid2center[search_layer_idx][search_hole_idx].first;
std::tuple<Point, float, int, coord_t, bool, int>& search_id = layerid2center[search_layer_idx][search_hole_idx].first;
if (std::get<2>(id) == std::get<2>(search_id)
&& std::get<0>(id).distance_to(std::get<0>(search_id)) < std::get<3>(id)
&& std::abs(std::get<1>(id) - std::get<1>(search_id)) < std::get<3>(id)
@@ -279,7 +281,7 @@ void PrintObject::_transform_hole_to_polyholes()
}
//create a polyhole per id and replace holes points by it.
for (auto entry : id2layerz2hole) {
Polygons polyholes = create_polyholes(std::get<0>(entry.first), std::get<1>(entry.first), scale_(print()->config().nozzle_diameter.get_at(std::get<2>(entry.first) - 1)), std::get<4>(entry.first));
Polygons polyholes = create_polyholes(std::get<0>(entry.first), std::get<1>(entry.first), scale_(print()->config().nozzle_diameter.get_at(std::get<2>(entry.first) - 1)), std::get<4>(entry.first), std::get<5>(entry.first));
for (auto& poly_to_replace : entry.second) {
Polygon polyhole = polyholes[poly_to_replace.second % polyholes.size()];
//search the clone in layers->slices
@@ -703,6 +705,72 @@ void PrintObject::infill()
if (this->set_started(posInfill)) {
m_print->set_status(35, L("Generating infill toolpath"));
// Orca: precompute the object's 3D connected bodies for separated infills / per-model
// centering. Two islands belong to the same body when their slices overlap on adjacent
// layers; islands that only overlap in top-down projection but never touch (e.g. interleaved
// chain links) stay separate, matching "split to objects". Each layer island then records
// the full bounding box of its body, so its infill is centered on that body as if it were
// sliced alone. Done once here, before the parallel fill, and only when a region needs it.
bool needs_separated_components = false;
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
const PrintRegionConfig &rc = this->printing_region(i).config();
if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) {
needs_separated_components = true;
break;
}
}
// Fast path: the feature only changes anything when the object is made of more than one
// connected body. Detect that cheaply the same way as "Split to objects" — more than one
// model part, or a single part whose mesh is splittable (is_splittable() is cached). A single
// body already shares the object center, i.e. the default, so skip the connectivity pass.
if (needs_separated_components) {
int parts = 0;
const ModelVolume *first_part = nullptr;
for (const ModelVolume *v : this->model_object()->volumes)
if (v->is_model_part()) { ++ parts; first_part = v; }
if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable()))
needs_separated_components = false;
}
for (Layer *layer : m_layers)
layer->lslices_separated_component_bboxes.clear();
if (needs_separated_components) {
const size_t nl = m_layers.size();
std::vector<size_t> offset(nl + 1, 0); // flat index of the first island of each layer
for (size_t i = 0; i < nl; ++ i)
offset[i + 1] = offset[i] + m_layers[i]->lslices.size();
const size_t nreg = offset[nl];
// Union-find over every (layer, island).
std::vector<size_t> parent(nreg);
for (size_t i = 0; i < nreg; ++ i) parent[i] = i;
auto find = [&parent](size_t x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
};
auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; };
// Join islands that overlap between two consecutive layers.
for (size_t i = 0; i + 1 < nl; ++ i) {
const Layer *la = m_layers[i], *lb = m_layers[i + 1];
for (size_t a = 0; a < la->lslices.size(); ++ a)
for (size_t b = 0; b < lb->lslices.size(); ++ b)
if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) &&
! intersection_ex(la->lslices[a], lb->lslices[b]).empty())
unite(offset[i] + a, offset[i + 1] + b);
}
// Full bounding box of each body, indexed by its union-find root.
std::vector<BoundingBox> body_bbox(nreg);
for (size_t i = 0; i < nl; ++ i)
for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a)
body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]);
// Store the body bbox for every island.
for (size_t i = 0; i < nl; ++ i) {
Layer *layer = m_layers[i];
layer->lslices_separated_component_bboxes.resize(layer->lslices.size());
for (size_t a = 0; a < layer->lslices.size(); ++ a)
layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)];
}
}
const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first;
const auto& support_fill_octree = this->m_adaptive_fill_octrees.second;
@@ -1111,6 +1179,8 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "outer_wall_speed"
|| opt_key == "small_perimeter_speed"
|| opt_key == "small_perimeter_threshold"
|| opt_key == "small_support_perimeter_speed"
|| opt_key == "small_support_perimeter_threshold"
|| opt_key == "sparse_infill_speed"
|| opt_key == "inner_wall_speed"
|| opt_key == "support_speed"
@@ -1203,6 +1273,7 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "hole_to_polyhole"
|| opt_key == "hole_to_polyhole_threshold"
|| opt_key == "hole_to_polyhole_twisted"
|| opt_key == "hole_to_polyhole_max_edges"
) {
steps.emplace_back(posSlice);
} else if (opt_key == "enable_support") {
@@ -1293,6 +1364,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_combination_max_layer_height"
|| opt_key == "bottom_shell_thickness"
|| opt_key == "top_shell_thickness"
|| opt_key == "top_surface_expansion"
|| opt_key == "top_surface_expansion_margin"
|| opt_key == "top_surface_expansion_direction"
|| opt_key == "minimum_sparse_infill_area"
|| opt_key == "sparse_infill_filament_id"
|| opt_key == "internal_solid_filament_id"
@@ -1303,6 +1377,8 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "skeleton_infill_line_width"
|| opt_key == "infill_direction"
|| opt_key == "solid_infill_direction"
|| opt_key == "top_layer_direction"
|| opt_key == "bottom_layer_direction"
|| opt_key == "align_infill_direction_to_model"
|| opt_key == "extra_solid_infills"
|| opt_key == "ensure_vertical_shell_thickness"
@@ -1317,6 +1393,8 @@ bool PrintObject::invalidate_state_by_config_options(
} else if (
opt_key == "top_surface_pattern"
|| opt_key == "bottom_surface_pattern"
|| opt_key == "top_surface_fill_order"
|| opt_key == "bottom_surface_fill_order"
|| opt_key == "internal_solid_infill_pattern"
|| opt_key == "external_fill_link_max_length"
|| opt_key == "infill_anchor"
@@ -1324,6 +1402,8 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_line_width"
|| opt_key == "top_surface_density"
|| opt_key == "bottom_surface_density"
|| opt_key == "center_of_surface_pattern"
|| opt_key == "separated_infills"
|| opt_key == "initial_layer_line_width"
|| opt_key == "small_area_infill_flow_compensation"
|| opt_key == "lateral_lattice_angle_1"
@@ -1424,6 +1504,8 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "outer_wall_speed"
|| opt_key == "small_perimeter_speed"
|| opt_key == "small_perimeter_threshold"
|| opt_key == "small_support_perimeter_speed"
|| opt_key == "small_support_perimeter_threshold"
|| opt_key == "sparse_infill_speed"
|| opt_key == "inner_wall_speed"
|| opt_key == "internal_solid_infill_speed"
@@ -1678,6 +1760,69 @@ void PrintObject::detect_surfaces_type()
}
}
// ORCA: Expand the top surfaces outward by top_surface_expansion in every direction. This
// enlarges the top solid infill and, in particular, grows it over the covered material left
// by features rising from the middle of a top surface (filling holes and joining tops so the
// features rest on it). The expansion stays inside the section it belongs to: each connected
// solid island has its own outer wall, so the top is grown within each island separately and
// clipped to it - growing one island's top across the gap into another island (which may have
// no top surface, leaving a partially filled layer) is never allowed. The top infill sits
// inside the perimeters, so the margin is measured from the walls: the island is inset by the
// band the walls consume (outer wall + inner walls) plus the configured margin, making that
// value the real clearance between the expanded top and the walls (avoiding a hull line). The
// original top is unioned back in, so where it already sits within that band it is kept as-is.
// Never claims a bottom surface.
const double top_expansion = layerm->region().config().top_surface_expansion.value;
if (top_expansion > 0. && ! top.empty()) {
const double d = scale_(top_expansion);
const auto jt = Clipper2Lib::JoinType::Miter;
const ExPolygons T = union_ex(to_expolygons(top));
const int wall_loops = layerm->region().config().wall_loops.value;
const double wall_band = wall_loops <= 0 ? 0. :
double(layerm->flow(frExternalPerimeter).scaled_width()) +
double(layerm->flow(frPerimeter).scaled_width()) * double(wall_loops - 1);
const double margin = scale_(layerm->region().config().top_surface_expansion_margin.value);
// minimum real top to act on: ignore anything thinner than ~2 top-infill lines
const float min_top = float(layerm->flow(frTopSolidInfill).scaled_width());
const auto direction = layerm->region().config().top_surface_expansion_direction.value;
ExPolygons grown;
for (const ExPolygon &island : union_ex(layerm_slices_surfaces)) {
// The top infill only exists inside the perimeters, so seed and measure from the infill
// region (the island minus the wall band), not the raw slice. A section whose only
// exposed top lies in the wall band - i.e. a layer where the top is just the walls
// themselves - has no infill here and is skipped, instead of being flooded inward by
// the expansion. Thin slivers inside the infill region are dropped by the opening too.
const ExPolygons infill_region = wall_band > 0. ? offset_ex(island, -float(wall_band)) : ExPolygons{ island };
const ExPolygons island_top = intersection_ex(T, infill_region);
if (opening_ex(island_top, min_top).empty())
continue; // no real top infill in this section - never expand into it
// grow by d, then keep only the part allowed by the configured direction: inward fills
// the holes/gaps left by features (clip the growth back to the top's own filled outline,
// which leaves the outer edge fixed), outward grows the outer edge toward the walls (drop
// the growth that fell into the original holes), and inward+outward keeps both.
ExPolygons expanded = offset_ex_2(island_top, d, jt);
if (direction != TopSurfaceExpansionDirection::InwardAndOutward) {
ExPolygons outline; // the top with its holes filled (same outer edge)
outline.reserve(island_top.size());
for (const ExPolygon &ex : island_top)
outline.emplace_back(ex.contour);
outline = union_ex(outline);
expanded = direction == TopSurfaceExpansionDirection::Inward ?
intersection_ex(expanded, outline) : // only growth into the holes
diff_ex(expanded, diff_ex(outline, island_top)); // only growth past the outer edge
}
// hold the expansion clear of the walls by the configured margin
const ExPolygons allowed = margin > 0. ? offset_ex(infill_region, -float(margin)) : infill_region;
append(grown, intersection_ex(expanded, allowed));
}
ExPolygons new_top = diff_ex(union_ex(T, grown), to_expolygons(bottom));
top.clear();
surfaces_append(top, std::move(new_top), stTop);
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
static int iRun = 0;
@@ -2174,7 +2319,7 @@ void PrintObject::discover_vertical_shells()
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
Flow solid_infill_flow = layerm->flow(frSolidInfill);
coord_t infill_line_spacing = solid_infill_flow.scaled_spacing();
coord_t infill_line_spacing = solid_infill_flow.scaled_spacing();
// Find a union of perimeters below / above this surface to guarantee a minimum shell thickness.
Polygons shell;
Polygons holes;
@@ -2216,7 +2361,7 @@ void PrintObject::discover_vertical_shells()
shell = std::move(shells2);
else if (! shells2.empty()) {
polygons_append(shell, shells2);
// Running the union_ using the Clipper library piece by piece is cheaper
// Running the union_ using the Clipper library piece by piece is cheaper
// than running the union_ all at once.
shell = union_(shell);
}
@@ -2283,12 +2428,12 @@ void PrintObject::discover_vertical_shells()
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
svg.draw(shell);
svg.draw_outline(shell, "black", scale_(0.05));
svg.Close();
svg.Close();
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
#if 0
// shell = union_(shell, true);
shell = union_(shell, false);
shell = union_(shell, false);
#endif
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
shell_ex = union_safety_offset_ex(shell);
@@ -2592,7 +2737,7 @@ void PrintObject::bridge_over_infill()
}
}
// LIGHTNING INFILL SECTION - If lightning infill is used somewhere, we check the areas that are going to be bridges, and those that rely on the
// LIGHTNING INFILL SECTION - If lightning infill is used somewhere, we check the areas that are going to be bridges, and those that rely on the
// lightning infill under them get expanded. This somewhat helps to ensure that most of the extrusions are anchored to the lightning infill at the ends.
// It requires modifying this instance of print object in a specific way, so that we do not invalidate the pointers in our surfaces_by_layer structure.
if (has_lightning_infill) {
@@ -3567,13 +3712,13 @@ static void clamp_feature_filament_to_valid(ConfigOptionInt &opt, size_t num_ext
opt.value = 1;
}
PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders)
PrintObjectConfig PrintObject::object_config_from_model_object(const PrintObjectConfig &default_object_config, const ModelObject &object, size_t num_extruders, std::vector<int>& variant_index)
{
PrintObjectConfig config = default_object_config;
{
DynamicPrintConfig src_normalized(object.config.get());
src_normalized.normalize_fdm();
config.apply(src_normalized, true);
update_static_print_config_from_dynamic(config, src_normalized, variant_index, print_options_with_variant, 1);
}
// Clamp invalid extruders to the default extruder (with index 1).
clamp_exturder_to_default(config.support_filament, num_extruders);
@@ -3601,7 +3746,7 @@ struct FeatureFilamentOverrideMask
bool inner_wall_filament_id = false;
};
static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in, FeatureFilamentOverrideMask &feature_overrides)
static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPrintConfig &in, FeatureFilamentOverrideMask &feature_overrides, std::vector<int>& variant_index)
{
// 1) Explicit feature filament values take precedence over base extruder fallback.
auto *opt_extruder = in.opt<ConfigOptionInt>(key_extruder);
@@ -3642,8 +3787,18 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
else if (it->first == "inner_wall_filament_id")
feature_overrides.inner_wall_filament_id = false;
}
} else
my_opt->set(it->second.get());
} else {
if (*my_opt != *(it->second)) {
if (my_opt->is_scalar() || variant_index.empty() || (print_options_with_variant.find(it->first) == print_options_with_variant.end()))
my_opt->set(it->second.get());
//my_opt->set(it->second.get());
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);
}
}
}
}
// 3) Apply base extruder only to features that were not explicitly overridden.
@@ -3663,7 +3818,7 @@ static void apply_to_print_region_config(PrintRegionConfig &out, const DynamicPr
}
}
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders)
PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &default_or_parent_region_config, const DynamicPrintConfig *layer_range_config, const ModelVolume &volume, size_t num_extruders, std::vector<int>& variant_index)
{
PrintRegionConfig config = default_or_parent_region_config;
FeatureFilamentOverrideMask feature_overrides;
@@ -3681,17 +3836,17 @@ PrintRegionConfig region_config_from_model_volume(const PrintRegionConfig &defau
if (volume.is_model_part()) {
// default_or_parent_region_config contains the Print's PrintRegionConfig.
// Override with ModelObject's PrintRegionConfig values.
apply_to_print_region_config(config, volume.get_object()->config.get(), feature_overrides);
apply_to_print_region_config(config, volume.get_object()->config.get(), feature_overrides, variant_index);
} else {
// default_or_parent_region_config contains parent PrintRegion config, which already contains ModelVolume's config.
}
apply_to_print_region_config(config, volume.config.get(), feature_overrides);
apply_to_print_region_config(config, volume.config.get(), feature_overrides, variant_index);
if (! volume.material_id().empty())
apply_to_print_region_config(config, volume.material()->config.get(), feature_overrides);
apply_to_print_region_config(config, volume.material()->config.get(), feature_overrides, variant_index);
if (layer_range_config != nullptr) {
// Not applicable to modifiers.
assert(volume.is_model_part());
apply_to_print_region_config(config, *layer_range_config, feature_overrides);
apply_to_print_region_config(config, *layer_range_config, feature_overrides, variant_index);
}
// Resolve feature defaults and clamp invalid extruders to index 1.
clamp_feature_filament_to_valid(config.sparse_infill_filament_id, num_extruders);
@@ -3741,7 +3896,7 @@ void PrintObject::update_slicing_parameters()
}
// Orca: XYZ shrinkage compensation has introduced the const Vec3d &object_shrinkage_compensation parameter to the function below
SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation)
SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full_config, const ModelObject &model_object, float object_max_z, const Vec3d &object_shrinkage_compensation, std::vector<int> variant_index)
{
PrintConfig print_config;
PrintObjectConfig object_config;
@@ -3751,14 +3906,14 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full
default_region_config.apply(full_config, true);
// BBS
size_t filament_extruders = print_config.filament_diameter.size();
object_config = object_config_from_model_object(object_config, model_object, filament_extruders);
object_config = object_config_from_model_object(object_config, model_object, filament_extruders, variant_index);
std::vector<unsigned int> object_extruders;
for (const ModelVolume* model_volume : model_object.volumes)
if (model_volume->is_model_part()) {
PrintRegion::collect_object_printing_extruders(
print_config,
region_config_from_model_volume(default_region_config, nullptr, *model_volume, filament_extruders),
region_config_from_model_volume(default_region_config, nullptr, *model_volume, filament_extruders, variant_index),
object_config.brim_type != btNoBrim && object_config.brim_width > 0.,
object_extruders);
for (const std::pair<const t_layer_height_range, ModelConfig> &range_and_config : model_object.layer_config_ranges)
@@ -3770,7 +3925,7 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full
range_and_config.second.has("bottom_surface_filament_id"))
PrintRegion::collect_object_printing_extruders(
print_config,
region_config_from_model_volume(default_region_config, &range_and_config.second.get(), *model_volume, filament_extruders),
region_config_from_model_volume(default_region_config, &range_and_config.second.get(), *model_volume, filament_extruders, variant_index),
object_config.brim_type != btNoBrim && object_config.brim_width > 0.,
object_extruders);
}
@@ -22,7 +22,7 @@ struct Params
: /*max_acceleration(max_acceleration), */raft_layers_count(raft_layers_count), brim_type(brim_type), brim_width(brim_width)
{
if (filament_types.size() > 1) {
BOOST_LOG_TRIVIAL(warning)
BOOST_LOG_TRIVIAL(debug)
<< "SupportSpotsGenerator does not currently handle different materials properly, only first will be used";
}
if (filament_types.empty() || filament_types[0].empty()) {
+7
View File
@@ -102,6 +102,9 @@ const std::string& var_dir();
// Return a full resource path for a file_name.
std::string var(const std::string &file_name);
// Snap a nozzle diameter to the closest supported value and format it as a string (e.g. 0.4 -> "0.4").
std::string format_diameter_to_str(double diameter, int precision = 1);
// Set a path with various static definition data (for example the initial config bundles).
void set_resources_dir(const std::string &path);
// Return a full path to the resources directory.
@@ -300,6 +303,10 @@ std::string header_gcodeviewer_generated();
// getpid platform wrapper
extern unsigned get_current_pid();
// Per-user id for isolating temp dirs; empty on Windows (its temp dir is already per-user).
std::string per_user_temp_id();
// Per-user temp root under `base`; an empty `user_id` returns `base` unchanged.
std::string per_user_temp_dir(const std::string &base, const std::string &user_id);
// BBS: backup & restore
std::string get_process_name(int pid);
+10
View File
@@ -83,6 +83,8 @@ public:
NozzleVolumeType nozzle_volume_type;
BedType bed_type;
float nozzle_diameter;
int nozzle_pos_id{-1};
std::string nozzle_sn;
std::string filament_id;
std::string setting_id;
std::string name;
@@ -93,6 +95,8 @@ public:
this->extruder_id = other.extruder_id;
this->nozzle_volume_type = other.nozzle_volume_type;
this->nozzle_diameter = other.nozzle_diameter;
this->nozzle_pos_id = other.nozzle_pos_id;
this->nozzle_sn = other.nozzle_sn;
this->filament_id = other.filament_id;
this->setting_id = other.setting_id;
this->name = other.name;
@@ -123,7 +127,9 @@ public:
int ams_id = 0;
int slot_id = 0;
int cali_idx = -1;
int nozzle_pos_id = -1; //-1 means no nozzle pos
float nozzle_diameter;
std::string nozzle_sn;
std::string filament_id;
std::string setting_id;
std::string name;
@@ -140,7 +146,9 @@ struct PACalibIndexInfo
int ams_id = 0;
int slot_id = 0;
int cali_idx = -1; // -1 means default
int nozzle_pos_id = -1; //-1 means no nozzle pos
float nozzle_diameter;
std::string nozzle_sn;
std::string filament_id;
};
@@ -148,7 +156,9 @@ struct PACalibExtruderInfo
{
int extruder_id = 0;
NozzleVolumeType nozzle_volume_type;
int nozzle_pos_id = -1; //-1 means no nozzle pos
float nozzle_diameter;
std::string nozzle_sn;
std::string filament_id = "";
bool use_extruder_id{true};
bool use_nozzle_volume_type{true};
+39
View File
@@ -1,6 +1,8 @@
#include <exception>
#include <cstdint>
#include "miniz_extension.hpp"
#include "Utils.hpp"
#if defined(_MSC_VER) || defined(__MINGW64__)
#include "boost/nowide/cstdio.hpp"
@@ -15,6 +17,33 @@
namespace Slic3r {
namespace {
std::string decode_zip_unicode_path_extra_field(const std::string& extra, const std::string& path)
{
size_t offset = 0;
const mz_uint32 path_crc = mz_crc32(0, reinterpret_cast<const unsigned char*>(path.data()), path.size());
while (offset + 4 <= extra.size()) {
const unsigned char* field = reinterpret_cast<const unsigned char*>(extra.data() + offset);
const std::uint16_t len = field[2] | (static_cast<std::uint16_t>(field[3]) << 8);
if (offset + 4 + len > extra.size())
break;
if (field[0] == 0x75 && field[1] == 0x70 && len >= 5 && field[4] == 0x01) {
const mz_uint32 stored_crc =
static_cast<mz_uint32>(field[5]) |
(static_cast<mz_uint32>(field[6]) << 8) |
(static_cast<mz_uint32>(field[7]) << 16) |
(static_cast<mz_uint32>(field[8]) << 24);
if (stored_crc == path_crc)
return std::string(extra.data() + offset + 9, extra.data() + offset + 4 + len);
}
offset += 4 + len;
}
return Slic3r::decode_path(path.c_str());
}
bool open_zip(mz_zip_archive *zip, const char *fname, bool isread)
{
if (!zip) return false;
@@ -76,6 +105,16 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname)
bool close_zip_reader(mz_zip_archive *zip) { return close_zip(zip, true); }
bool close_zip_writer(mz_zip_archive *zip) { return close_zip(zip, false); }
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat)
{
if (stat.m_is_utf8)
return stat.m_filename;
std::string extra(1024, 0);
const size_t extra_size = mz_zip_reader_get_extra(zip, stat.m_file_index, extra.data(), extra.size());
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
}
MZ_Archive::MZ_Archive()
{
mz_zip_zero_struct(&arch);
+1
View File
@@ -10,6 +10,7 @@ bool open_zip_reader(mz_zip_archive *zip, const std::string &fname_utf8);
bool open_zip_writer(mz_zip_archive *zip, const std::string &fname_utf8);
bool close_zip_reader(mz_zip_archive *zip);
bool close_zip_writer(mz_zip_archive *zip);
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
class MZ_Archive {
public:
+31
View File
@@ -8,6 +8,10 @@
#include <iostream>
#include <stdio.h>
#include <filesystem>
#include <sstream>
#include <iomanip>
#include <algorithm>
#include <cmath>
#include "format.hpp"
#include "Platform.hpp"
@@ -1285,6 +1289,24 @@ unsigned get_current_pid()
#endif
}
std::string per_user_temp_id()
{
#ifdef WIN32
return {};
#else
return std::to_string(static_cast<unsigned long>(::getuid()));
#endif
}
std::string per_user_temp_dir(const std::string &base, const std::string &user_id)
{
if (user_id.empty())
return base;
// Keep the id at the top level so each user's dir sits directly in the world-writable temp
// root; a shared parent dir would be owned by whichever user created it first.
return base + "/orcaslicer_" + user_id;
}
// BBS: backup & restore
std::string get_process_name(int pid)
{
@@ -1479,6 +1501,15 @@ std::string format_memsize(size_t bytes, unsigned int decimals)
}
}
std::string format_diameter_to_str(double diameter, int precision)
{
double candidates[] = {0.2, 0.4, 0.6, 0.8};
double best = *std::min_element(std::begin(candidates), std::end(candidates), [diameter](double a, double b) { return std::abs(a - diameter) < std::abs(b - diameter); });
std::ostringstream oss;
oss << std::fixed << std::setprecision(precision) << best;
return oss.str();
}
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
// The latter parses the string with (semi)colons as separators, it should look about as
// "desc1: value1; desc2: value2" or similar (spaces should not matter).