Merge upstream/main into iXex PR branch (#13086)

Catches the iXex/IDEX parallel-printing branch up to upstream main
(102 commits). Two content conflicts resolved:

- src/libslic3r/Preset.cpp: s_Preset_printer_options — kept upstream's
  new "use_3mf" key and our iMEX printer-capability/mode keys.
- tests/fff_print/test_gcodewriter.cpp: upstream revived the disabled
  suite (#14196), dropping the obsolete [.]-tagged lift() test and its
  config_lift_unlift.ini; kept their set_speed + z_hop tests and appended
  our 10 per-firmware set_pressure_advance/set_temperature scenarios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clifford Garwood
2026-06-26 23:29:35 -04:00
co-authored by Claude Opus 4.8
888 changed files with 103247 additions and 195853 deletions
+365 -201
View File
@@ -24,7 +24,9 @@
#include <algorithm>
#include <limits>
#include <numeric>
#include <unordered_set>
#include <sstream>
#include <boost/filesystem/path.hpp>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
@@ -238,7 +240,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"bed_temperature_formula",
"filament_notes",
"process_notes",
"printer_notes"
"printer_notes",
"use_3mf"
};
static std::unordered_set<std::string> steps_ignore;
@@ -670,13 +673,13 @@ StringObjectException Print::sequential_print_clearance_valid(const Print &print
//juedge the exclude area
if (!intersection(exclude_polys, convex_hull_no_offset).empty()) {
if (single_object_exception.string.empty()) {
single_object_exception.string = (boost::format(L("%1% is too close to exclusion area, there may be collisions when printing.")) %instance.model_instance->get_object()->name).str();
single_object_exception.string = (boost::format(L("%1% is too close to exclusion area. There may be collisions when printing.")) %instance.model_instance->get_object()->name).str();
// single_object_exception.object = instance.model_instance->get_object();
//ORCA: Pass ModelInstance instead of ModelObject
single_object_exception.object = instance.model_instance;
}
else {
single_object_exception.string += "\n"+(boost::format(L("%1% is too close to exclusion area, there may be collisions when printing.")) %instance.model_instance->get_object()->name).str();
single_object_exception.string += "\n"+(boost::format(L("%1% is too close to exclusion area. There may be collisions when printing.")) %instance.model_instance->get_object()->name).str();
single_object_exception.object = nullptr;
}
//if (polygons) {
@@ -1039,7 +1042,7 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
/*if (warning) {
warning->string += L("Prime Tower is too close to exclusion area, there may be collisions when printing.\n");
}*/
return {L("Prime Tower") + L(" is too close to exclusion area, and collisions will be caused.\n")};
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")};
}
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) {
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
@@ -1256,9 +1259,22 @@ StringObjectException Print::check_multi_filament_valid(const Print& print)
}
// Precondition: Print::validate() requires the Print::apply() to be called its invocation.
//BBS: refine seq-print validation logic.....FIXME:StringObjectException *warning can only contain one warning, but there might be many warnings, need a vector<StringObjectException>
StringObjectException Print::validate(StringObjectException *warning, Polygons* collison_polygons, std::vector<std::pair<Polygon, float>>* height_polygons) const
//BBS: refine seq-print validation logic
StringObjectException Print::validate(std::vector<StringObjectException> *warnings, Polygons* collison_polygons, std::vector<std::pair<Polygon, float>>* height_polygons) const
{
auto add_warning = [warnings](StringObjectException w) {
w.is_warning = true;
if (warnings != nullptr)
warnings->push_back(std::move(w));
};
auto warn = [&](std::string msg, std::string opt_key = "", const ObjectBase* object = nullptr) {
StringObjectException w;
w.string = std::move(msg);
w.opt_key = std::move(opt_key);
w.object = object;
add_warning(std::move(w));
};
std::vector<unsigned int> extruders = this->extruders();
unsigned int nozzles = m_config.nozzle_diameter.size();
@@ -1305,9 +1321,8 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
if (!ret.string.empty())
{
ret.type = STRING_EXCEPT_FILAMENTS_DIFFERENT_TEMP;
if (ret.is_warning && warning != nullptr) {
*warning = ret;
//return {};
if (ret.is_warning) {
add_warning(ret);
}else
return ret;
}
@@ -1333,32 +1348,26 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
}
else {
//BBS
auto ret = layered_print_cleareance_valid(*this, warning);
StringObjectException layer_warning;
auto ret = layered_print_cleareance_valid(*this, &layer_warning);
if (!ret.string.empty()) {
ret.type = STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT;
return ret;
}
if (!layer_warning.string.empty())
add_warning(layer_warning);
}
if (m_config.enable_prime_tower) {
for (const PrintObject* object : m_objects) {
if (object->config().precise_z_height.value && warning != nullptr) {
StringObjectException warningtemp;
warningtemp.string = L("Enabling both precise Z height and the prime tower may cause slicing errors.");
warningtemp.opt_key = "precise_z_height";
warningtemp.is_warning = true;
*warning = warningtemp;
if (object->config().precise_z_height.value) {
warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height");
break;
}
}
} else {
if (m_config.enable_wrapping_detection && warning!=nullptr) {
StringObjectException warningtemp;
warningtemp.string = L("A prime tower is required for clumping detection; otherwise, there may be flaws on the model.");
warningtemp.opt_key = "enable_prime_tower";
warningtemp.is_warning = true;
*warning = warningtemp;
}
if (m_config.enable_wrapping_detection)
warn(L("A prime tower is required for clumping detection; otherwise, there may be flaws on the model."), "enable_prime_tower");
}
if (m_config.spiral_mode) {
@@ -1375,7 +1384,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
if (std::any_of(all_regions.begin() + 1, all_regions.end(), [ra = all_regions.front()](const auto rb) {
return !Layer::is_perimeter_compatible(ra, rb);
})) {
return {L("The spiral vase mode does not work when an object contains more than one materials."), nullptr, "spiral_mode"};
return {L("Spiral (vase) mode does not work when an object contains more than one material."), nullptr, "spiral_mode"};
}
}
}
@@ -1454,8 +1463,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
if (nozzle_diam - EPSILON > first_nozzle_diam || nozzle_diam + EPSILON < first_nozzle_diam
|| std::abs((filament_diam - first_filament_diam) / first_filament_diam) > 0.1) {
// return { L("Different nozzle diameters and different filament diameters may not work well when prime tower is enabled. It's very experimental, please proceed with caucious.") };
warning->string = L("Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution.");
warning->opt_key = "nozzle_diameter";
warn(L("Different nozzle diameters and different filament diameters may not work well when the prime tower is enabled. It's very experimental, so please proceed with caution."), "nozzle_diameter");
break;
}
}
@@ -1472,14 +1480,14 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
return { L("The prime tower is currently only supported for the Marlin, RepRap/Sprinter, RepRapFirmware and Repetier G-code flavors.")};
if ((m_config.print_sequence == PrintSequence::ByObject) && extruders.size() > 1)
return { L("The prime tower is not supported in \"By object\" print."), nullptr, "enable_prime_tower" };
return { L("A prime tower is not supported in \u201cBy object\u201d print."), nullptr, "enable_prime_tower" };
// BBS: When prime tower is on, object layer and support layer must be aligned. So support gap should be multiple of object layer height.
for (size_t i = 0; i < m_objects.size(); i++) {
const PrintObject* object = m_objects[i];
const SlicingParameters& slicing_params = object->slicing_parameters();
if (object->config().adaptive_layer_height) {
return { L("The prime tower is not supported when adaptive layer height is on. It requires that all objects have the same layer height."), object, "adaptive_layer_height" };
return { L("A prime tower is not supported when adaptive layer height is on. It requires that all objects have the same layer height."), object, "adaptive_layer_height" };
}
if (!object->config().enable_support)
@@ -1487,7 +1495,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
double gap_layers = slicing_params.gap_object_support / slicing_params.layer_height;
if (gap_layers - (int)gap_layers > EPSILON) {
return {L("The prime tower requires \"support gap\" to be multiple of layer height."), object};
return {L("A prime tower requires any \u201csupport gap\u201d to be a multiple of layer height."), object};
}
}
#endif
@@ -1500,9 +1508,9 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
const SlicingParameters &slicing_params = object->slicing_parameters();
if (std::abs(slicing_params.first_print_layer_height - slicing_params0.first_print_layer_height) > EPSILON ||
std::abs(slicing_params.layer_height - slicing_params0.layer_height ) > EPSILON)
return {L("The prime tower requires that all objects have the same layer heights."), object, "initial_layer_print_height"};
return {L("A prime tower requires that all objects have the same layer height."), object, "initial_layer_print_height"};
if (slicing_params.raft_layers() != slicing_params0.raft_layers())
return {L("The prime tower requires that all objects are printed over the same number of raft layers."), object, "raft_layers"};
return {L("A prime tower requires that all objects are printed over the same number of raft layers."), object, "raft_layers"};
// BBS: support gap can be multiple of object layer height, remove _L()
#if 0
if (slicing_params0.gap_object_support != slicing_params.gap_object_support ||
@@ -1510,7 +1518,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
return {L("The prime tower is only supported for multiple objects if they are printed with the same support_top_z_distance."), object};
#endif
if (!equal_layering(slicing_params, slicing_params0))
return { L("The prime tower requires that all objects are sliced with the same layer heights."), object };
return { L("A prime tower requires that all objects are sliced with the same layer height."), object };
if (has_custom_layering) {
auto &lh = layer_height_profile(i);
auto &lh_tallest = layer_height_profile(tallest_object_idx);
@@ -1576,10 +1584,10 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
if (extrusion_width_min == 0) {
// Default "auto-generated" extrusion width is always valid.
} else if (extrusion_width_min <= layer_height) {
err_msg = L("Too small line width");
err_msg = L("Line width too small");
return false;
} else if (extrusion_width_max > max_nozzle_diameter * MAX_LINE_WIDTH_MULTIPLIER) {
err_msg = L("Too large line width");
err_msg = L("Line width too large");
return false;
}
return true;
@@ -1601,7 +1609,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
// BBS
#if 0
if (this->has_wipe_tower() && object->config().independent_support_layer_height) {
return {L("The prime tower requires that support has the same layer height with object."), object, "support_filament"};
return {L("A prime tower requires that support has the same layer height as the object."), object, "support_filament"};
}
#endif
@@ -1612,22 +1620,15 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
// Orca: use organic as default
object->config().support_style == smsDefault) {
if (warning) {
// Orca: check the support wall count and the base pattern
if (object->config().tree_support_wall_count > 1 &&
object->config().support_base_pattern != SupportMaterialPattern::smpNone &&
object->config().support_base_pattern != SupportMaterialPattern::smpDefault) {
warning->string = L("For Organic supports, two walls are supported only with the Hollow/Default base pattern.");
warning->opt_key = "support_base_pattern";
}
// Orca: check the support wall count and the base pattern
if (object->config().tree_support_wall_count > 1 &&
object->config().support_base_pattern != SupportMaterialPattern::smpNone &&
object->config().support_base_pattern != SupportMaterialPattern::smpDefault)
warn(L("For Organic supports, two walls are supported only with the Hollow/Default base pattern."), "support_base_pattern");
// Orca: check if the Lightning base pattern selected
if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning) {
warning->string = L(
"The Lightning base pattern is not supported by this support type; Rectilinear will be used instead.");
warning->opt_key = "support_base_pattern";
}
}
// Orca: check if the Lightning base pattern selected
if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning)
warn(L("The Lightning base pattern is not supported by this support type; Rectilinear will be used instead."), "support_base_pattern");
float extrusion_width = std::min(
support_material_flow(object).width(),
@@ -1639,28 +1640,23 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
if (object->config().tree_support_branch_diameter_organic < object->config().tree_support_tip_diameter)
return { L("Organic support branch diameter must not be smaller than support tree tip diameter."), object, "tree_support_branch_diameter_organic" };
}
} else if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning && warning) {
} else if (object->config().support_base_pattern == SupportMaterialPattern::smpLightning) {
// Orca: check if the Lightning base pattern selected
warning->string = L("The Lightning base pattern is not supported by this support type; Rectilinear will be used instead.");
warning->opt_key = "support_base_pattern";
} else if (object->config().support_base_pattern == SupportMaterialPattern::smpNone && warning) {
warn(L("The Lightning base pattern is not supported by this support type; Rectilinear will be used instead."), "support_base_pattern");
} else if (object->config().support_base_pattern == SupportMaterialPattern::smpNone) {
// Orca: check if the Hollow base pattern selected
warning->string = L("The Hollow base pattern is not supported by this support type; Rectilinear will be used instead.");
warning->opt_key = "support_base_pattern";
warn(L("The Hollow base pattern is not supported by this support type; Rectilinear will be used instead."), "support_base_pattern");
}
}
// Do we have custom support data that would not be used?
// Notify the user in that case.
if (! object->has_support() && warning) {
if (! object->has_support()) {
for (const ModelVolume* mv : object->model_object()->volumes) {
bool has_enforcers = mv->is_support_enforcer() ||
(mv->is_model_part() && mv->supported_facets.has_facets(*mv, EnforcerBlockerType::ENFORCER));
if (has_enforcers) {
StringObjectException warningtemp;
warningtemp.string = L("Support enforcers are used but support is not enabled. Please enable support.");
warningtemp.object = object;
*warning = warningtemp;
warn(L("Support enforcers are used but support is not enabled. Please enable support."), "", object);
break;
}
}
@@ -1714,7 +1710,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
return { err_msg, object, "bridge_line_width" };
}
if (!allow_thin_bridge_width && bridge_width <= layer_height) {
err_msg = L("Too small line width");
err_msg = L("Line width too small");
return { err_msg, object, "bridge_line_width" };
}
}
@@ -1812,7 +1808,11 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
}
// check if print speed/accel/jerk is higher than the maximum speed of the printer
if (warning) {
if (warnings) {
// The motion-ability checks are mutually exclusive (gated on warning_key), so collect the
// single one that fires into a local and push it once - separate from the precise-wall and
// shrinkage warnings below.
StringObjectException motion_warning;
try {
auto check_motion_ability_object_setting = [&](const std::vector<std::string>& keys_to_check, double limit) -> std::string {
std::string warning_key;
@@ -1843,7 +1843,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
if (!ignore_jerk_validation) {
if (m_default_object_config.default_jerk == 1 || m_default_object_config.outer_wall_jerk == 1 ||
m_default_object_config.inner_wall_jerk == 1) {
warning->string = L("Setting the jerk speed too low could lead to artifacts on curved surfaces");
motion_warning.string = L("Setting the jerk speed too low could lead to artifacts on curved surfaces");
if (m_default_object_config.outer_wall_jerk == 1)
warning_key = "outer_wall_jerk";
else if (m_default_object_config.inner_wall_jerk == 1)
@@ -1851,7 +1851,7 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
else
warning_key = "default_jerk";
warning->opt_key = warning_key;
motion_warning.opt_key = warning_key;
}
if (warning_key.empty() && m_default_object_config.default_jerk > 0) {
@@ -1861,23 +1861,20 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
warning_key.clear();
warning_key = check_motion_ability_object_setting(jerk_to_check, max_jerk);
if (!warning_key.empty()) {
warning->string = L(
motion_warning.string = L(
"The jerk setting exceeds the printer's maximum jerk (machine_max_jerk_x/machine_max_jerk_y).\n"
"Orca will automatically cap the jerk speed to ensure it doesn't surpass the printer's capabilities.\n"
"You can adjust the maximum jerk setting in your printer's configuration to get higher speeds.");
warning->opt_key = warning_key;
motion_warning.opt_key = warning_key;
}
}
}
// Check junction deviation
// Orca: Only marlin FW supports max junction deviation. Dont display warning if firmware is not supporting it.
const bool support_max_junction_deviation = ( m_config.gcode_flavor == gcfMarlinFirmware);
if (warning_key.empty() && m_default_object_config.default_junction_deviation.value > max_junction_deviation && support_max_junction_deviation) {
warning->string = L( "Junction deviation setting exceeds the printer's maximum value (machine_max_junction_deviation).\n"
// check junction deviation
else if (m_default_object_config.default_junction_deviation.value > max_junction_deviation) {
motion_warning.string = L( "Junction deviation setting exceeds the printer's maximum value (machine_max_junction_deviation).\n"
"Orca will automatically cap the junction deviation to ensure it doesn't surpass the printer's capabilities.\n"
"You can adjust the machine_max_junction_deviation value in your printer's configuration to get higher limits.");
warning->opt_key = warning_key;
motion_warning.opt_key = "default_junction_deviation";
}
// check acceleration
@@ -1912,12 +1909,12 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
};
warning_key = check_motion_ability_object_setting(accel_to_check, max_accel);
if (!warning_key.empty()) {
warning->string = L("The acceleration setting exceeds the printer's maximum acceleration "
motion_warning.string = L("The acceleration setting exceeds the printer's maximum acceleration "
"(machine_max_acceleration_extruding).\nOrca will "
"automatically cap the acceleration speed to ensure it doesn't surpass the printer's "
"capabilities.\nYou can adjust the "
"machine_max_acceleration_extruding value in your printer's configuration to get higher speeds.");
warning->opt_key = warning_key;
motion_warning.opt_key = warning_key;
}
if (support_travel_acc) {
const auto max_travel = m_config.machine_max_acceleration_travel.values[0];
@@ -1927,13 +1924,13 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
};
warning_key = check_motion_ability_object_setting(accel_to_check, max_travel);
if (!warning_key.empty()) {
warning->string = L(
motion_warning.string = L(
"The travel acceleration setting exceeds the printer's maximum travel acceleration "
"(machine_max_acceleration_travel).\nOrca will "
"automatically cap the travel acceleration speed to ensure it doesn't surpass the printer's "
"capabilities.\nYou can adjust the "
"machine_max_acceleration_travel value in your printer's configuration to get higher speeds.");
warning->opt_key = warning_key;
motion_warning.opt_key = warning_key;
}
}
}
@@ -1959,19 +1956,34 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
// }
// check wall sequence and precise outer wall
if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter) {
warning->string = L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences.");
warning->opt_key = "precise_outer_wall";
if (m_default_region_config.precise_outer_wall && m_default_region_config.wall_sequence != WallSequence::InnerOuter)
warn(L("The precise wall option will be ignored for outer-inner or inner-outer-inner wall sequences."), "precise_outer_wall");
// check adaptive pressure advance model
for (unsigned int extruder_id : extruders) {
if (m_config.adaptive_pressure_advance.get_at(extruder_id) &&
m_config.enable_pressure_advance.get_at(extruder_id)) {
const std::string pa_model = m_config.adaptive_pressure_advance_model.get_at(extruder_id);
if (!pa_model.empty()) {
std::string validation_error = AdaptivePAProcessor::validate_adaptive_pa_model(pa_model);
if (!validation_error.empty()) {
warn(L("The Adaptive Pressure Advance model for one or more extruders may contain invalid values."),
"adaptive_pressure_advance_model");
break;
}
}
}
}
} catch (std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "Orca: validate motion ability failed: " << e.what() << std::endl;
}
if (!motion_warning.string.empty())
add_warning(motion_warning);
}
if (!this->has_same_shrinkage_compensations()){
warning->string = L("Filament shrinkage will not be used because filament shrinkage for the used filaments does not match.");
warning->opt_key = "";
}
if (!this->has_same_shrinkage_compensations())
warn(L("Filament shrinkage will not be used because filament shrinkage for the used filaments does not match."));
return {};
}
@@ -2060,21 +2072,23 @@ Flow Print::brim_flow() const
Flow Print::skirt_flow() const
{
// Orca: fall back to m_config if no objects are present
ConfigOptionFloatOrPercent width = m_config.initial_layer_line_width;
if (width.value <= 0)
width = m_objects.front()->config().line_width;
width = m_objects.empty() ? m_config.initial_layer_line_width : m_objects.front()->config().line_width;
/* We currently use a random object's support material extruder.
While this works for most cases, we should probably consider all of the support material
extruders and take the one with, say, the smallest index;
The same logic should be applied to the code that selects the extruder during G-code
generation as well. */
return Flow::new_from_config_width(
frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
(float)m_config.nozzle_diameter.get_at(m_objects.front()->config().support_filament-1),
(float)this->skirt_first_layer_height());
return Flow::new_from_config_width(frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
(float) m_config.nozzle_diameter.get_at(
m_objects.empty() ? 0 : m_objects.front()->config().support_filament - 1),
(float) this->skirt_first_layer_height());
}
bool Print::has_support_material() const
@@ -2477,18 +2491,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
start_time = (long long)Slic3r::Utils::get_current_time_utc();
m_skirt.clear();
m_skirt_brim_groups.clear();
m_has_shared_per_object_skirt = false;
m_skirt_convex_hull.clear();
m_objectBrimAreas.clear();
m_supportBrimAreas.clear();
m_first_layer_convex_hull.points.clear();
for (PrintObject *object : m_objects) object->m_skirt.clear();
const bool draft_shield = config().draft_shield != dsDisabled;
if (this->has_skirt() && draft_shield) {
// In case that draft shield is active, generate skirt first so brim
// can be trimmed to make room for it.
_make_skirt();
}
//BBS: get the objects' indices when GCodes are generated
ToolOrdering tool_ordering;
unsigned int initial_extruder_id = (unsigned int)-1;
@@ -2571,7 +2581,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (this->has_brim()) {
Polygons islands_area;
make_brim(*this, this->make_try_cancel(), islands_area, m_brimMap,
m_supportBrimMap, objPrintVec, printExtruders);
m_supportBrimMap, objPrintVec, printExtruders, &m_objectBrimAreas, &m_supportBrimAreas);
for (Polygon& poly_ex : islands_area)
poly_ex.douglas_peucker(SCALED_RESOLUTION);
for (Polygon &poly : union_(this->first_layer_islands(), islands_area))
@@ -2579,11 +2589,16 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
if (has_skirt() && ! draft_shield) {
// In case that draft shield is NOT active, generate skirt now.
// It will be placed around the brim, so brim has to be ready.
if (has_skirt() || has_infinite_skirt() || has_brim()) {
// Generate skirt/brim groups after brim so per-object and draft-shield footprints
// include brims when grouping and offsetting skirt loops.
assert(m_skirt.empty());
_make_skirt();
if (m_config.print_sequence == PrintSequence::ByObject &&
m_config.skirt_type == stPerObject &&
this->has_shared_per_object_skirt()) {
throw Slic3r::SlicingError(L("Per-object skirts cannot fit between the objects in By object print sequence.\n\nMove the objects farther apart, reduce brim/skirt size, switch Skirt type to Combined, or switch Print sequence to By layer."));
}
}
this->finalize_first_layer_convex_hull();
@@ -2673,12 +2688,15 @@ 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
result->conflict_result = m_conflict_result;
if (result != nullptr)
result->conflict_result = m_conflict_result;
return path.c_str();
}
void Print::_make_skirt()
{
const bool generate_skirt = this->has_skirt() || this->has_infinite_skirt();
// First off we need to decide how tall the skirt must be.
// The skirt_height option from config is expressed in layers, but our
// object might have different layer heights, so we need to find the print_z
@@ -2690,23 +2708,27 @@ void Print::_make_skirt()
// prepended to the first 'n' layers (with 'n' = skirt_height).
// $skirt_height_z in this case is the highest possible skirt height for safety.
coordf_t skirt_height_z = 0.;
for (const PrintObject *object : m_objects) {
size_t skirt_layers = this->has_infinite_skirt() ?
object->layer_count() :
std::min(size_t(m_config.skirt_height.value), object->layer_count());
skirt_height_z = std::max(skirt_height_z, object->m_layers[skirt_layers-1]->print_z);
if (generate_skirt) {
for (const PrintObject *object : m_objects) {
size_t skirt_layers = this->has_infinite_skirt() ?
object->layer_count() :
std::min(size_t(m_config.skirt_height.value), object->layer_count());
skirt_height_z = std::max(skirt_height_z, object->m_layers[skirt_layers-1]->print_z);
}
}
// Collect points from all layers contained in skirt height.
Points points;
struct ObjectSkirtHull {
PrintObject* object;
Polygon hull;
};
// BBS
std::map<PrintObject*, Polygon> object_convex_hulls;
// Orca: build one local occupied hull per object from object and support geometry up to skirt height.
std::vector<ObjectSkirtHull> object_convex_hulls;
for (PrintObject *object : m_objects) {
Points object_points;
// Get object layers up to skirt_height_z.
for (const Layer *layer : object->m_layers) {
if (layer->print_z > skirt_height_z)
if (generate_skirt && layer->print_z > skirt_height_z)
break;
for (const ExPolygon &expoly : layer->lslices)
// Collect the outer contour points only, ignore holes for the calculation of the convex hull.
@@ -2714,35 +2736,18 @@ void Print::_make_skirt()
}
// Get support layers up to skirt_height_z.
for (const SupportLayer *layer : object->support_layers()) {
if (layer->print_z > skirt_height_z)
if (generate_skirt && layer->print_z > skirt_height_z)
break;
layer->support_fills.collect_points(object_points);
}
object_convex_hulls.insert({ object, Slic3r::Geometry::convex_hull(object_points) });
// Repeat points for each object copy.
for (const PrintInstance &instance : object->instances()) {
Points copy_points = object_points;
for (Point &pt : copy_points)
pt += instance.shift;
append(points, copy_points);
}
object_convex_hulls.push_back({ object, Slic3r::Geometry::convex_hull(object_points) });
}
// Include the wipe tower.
append(points, this->first_layer_wipe_tower_corners());
// Unless draft shield is enabled, include all brims as well.
if (config().draft_shield == dsDisabled)
append(points, m_first_layer_convex_hull.points);
if (points.size() < 3)
// At least three points required for a convex hull.
if (object_convex_hulls.empty())
return;
this->throw_if_canceled();
Polygon convex_hull = Slic3r::Geometry::convex_hull(points);
// Skirt may be printed on several layers, having distinct layer heights,
// but loops must be aligned so can't vary width/spacing
@@ -2764,13 +2769,13 @@ void Print::_make_skirt()
}
}
// Initial offset of the brim inner edge from the object (possible with a support & raft).
// The skirt will touch the brim if the brim is extruded.
auto distance = float(scale_(m_config.skirt_distance.value - spacing/2.));
// Draw outlines from outside to inside.
// Initial skirt centerline offset from the occupied outline.
// The skirt will touch the occupied outline if skirt_distance is zero.
// Generate loops inward to outward; callers reverse them before G-code export.
// Loop while we have less skirts than required or any extruder hasn't reached the min length if any.
std::vector<coordf_t> extruded_length(extruders.size(), 0.);
if (m_config.skirt_type == stCombined) {
auto append_skirt_loops_for_hull = [&](const Polygon& hull, ExtrusionEntityCollection& dst, bool collect_skirt_hull) {
float distance = float(scale_(m_config.skirt_distance.value - spacing/2.));
std::vector<coordf_t> extruded_length(extruders.size(), 0.);
for (size_t i = m_config.skirt_loops, extruder_idx = 0; i > 0; -- i) {
this->throw_if_canceled();
// Offset the skirt outside.
@@ -2778,8 +2783,8 @@ void Print::_make_skirt()
// Generate the skirt centerline.
Polygon loop;
{
// BBS. skirt_distance is defined as the gap between skirt and outer most brim, so no need to add max_brim_width
Polygons loops = offset(convex_hull, distance, ClipperLib::jtRound, float(scale_(0.1)));
// Orca: the hull already represents the occupied outline used for this skirt.
Polygons loops = offset(hull, distance, ClipperLib::jtRound, float(scale_(0.1)));
Geometry::simplify_polygons(loops, scale_(0.05), &loops);
if (loops.empty())
break;
@@ -2795,7 +2800,7 @@ void Print::_make_skirt()
(float)initial_layer_print_height // this will be overridden at G-code export time
)));
eloop.paths.back().polyline = Polyline3(loop.split_at_first_point());
m_skirt.append(eloop);
dst.append(eloop);
if (m_config.min_skirt_length.value > 0) {
// The skirt length is limited. Sum the total amount of filament length extruded, in mm.
extruded_length[extruder_idx] += unscale<double>(loop.length()) * extruders_e_per_mm[extruder_idx];
@@ -2811,69 +2816,228 @@ void Print::_make_skirt()
++ extruder_idx;
}
} else {
// The skirt lenght is not limited, extrude the skirt with the 1st extruder only.
// The skirt length is not limited, extrude the skirt with the 1st extruder only.
}
}
} else {
m_skirt.clear();
}
// Brims were generated inside out, reverse to print the outmost contour first.
m_skirt.reverse();
// Remember the outer edge of the last skirt line extruded as m_skirt_convex_hull.
for (Polygon &poly : offset(convex_hull, distance + 0.5f * float(scale_(spacing)), ClipperLib::jtRound, float(scale_(0.1))))
append(m_skirt_convex_hull, std::move(poly.points));
if (collect_skirt_hull)
for (Polygon &poly : offset(hull, distance + 0.5f * float(scale_(spacing)), ClipperLib::jtRound, float(scale_(0.1))))
append(m_skirt_convex_hull, std::move(poly.points));
};
if (m_config.skirt_type == stPerObject) {
// BBS
for (auto obj_cvx_hull : object_convex_hulls) {
double object_skirt_distance = float(scale_(m_config.skirt_distance.value - spacing/2.));
PrintObject* object = obj_cvx_hull.first;
object->m_skirt.clear();
extruded_length.assign(extruded_length.size(), 0.);
for (size_t i = m_config.skirt_loops.value, extruder_idx = 0; i > 0; -- i) {
object_skirt_distance += float(scale_(spacing));
Polygon loop;
{
// BBS. skirt_distance is defined as the gap between skirt and outer most brim, so no need to add max_brim_width
Polygons loops = offset(obj_cvx_hull.second, object_skirt_distance, ClipperLib::jtRound, float(scale_(0.1)));
Geometry::simplify_polygons(loops, scale_(0.05), &loops);
if (loops.empty())
break;
loop = loops.front();
}
m_skirt.clear();
m_skirt_brim_groups.clear();
m_has_shared_per_object_skirt = false;
for (const ObjectSkirtHull& object_hull : object_convex_hulls)
object_hull.object->m_skirt.clear();
// Extrude the skirt loop.
ExtrusionLoop eloop(elrSkirt);
eloop.paths.emplace_back(ExtrusionPath(
ExtrusionPath(
erSkirt,
(float)mm3_per_mm, // this will be overridden at G-code export time
flow.width(),
(float)initial_layer_print_height // this will be overridden at G-code export time
)));
eloop.paths.back().polyline = Polyline3(loop.split_at_first_point());
object->m_skirt.append(std::move(eloop));
if (m_config.min_skirt_length.value > 0) {
// The skirt length is limited. Sum the total amount of filament length extruded, in mm.
extruded_length[extruder_idx] += unscale<double>(loop.length()) * extruders_e_per_mm[extruder_idx];
if (extruded_length[extruder_idx] < m_config.min_skirt_length.value) {
// Not extruded enough yet with the current extruder. Add another loop.
if (i == 1)
++ i;
} else {
assert(extruded_length[extruder_idx] >= m_config.min_skirt_length.value);
// Enough extruded with the current extruder. Extrude with the next one,
// until the prescribed number of skirt loops is extruded.
if (extruder_idx + 1 < extruders.size())
++ extruder_idx;
}
} else {
// The skirt lenght is not limited, extrude the skirt with the 1st extruder only.
}
if (m_config.skirt_type == stCombined || m_config.skirt_type == stPerObject) {
struct SkirtGroupItem {
Points occupied_points;
ObjectID object_id;
bool emits_skirt;
};
// Orca: group items represent occupied first-layer areas. Object items emit skirts;
// obstacle-only items, such as wipe tower, only force nearby object groups to merge.
std::vector<SkirtGroupItem> group_items;
const coord_t grouping_offset = scale_(m_config.skirt_distance.value + m_config.skirt_loops.value * spacing);
for (const ObjectSkirtHull& object_hull : object_convex_hulls) {
PrintObject* object = object_hull.object;
Points occupied_points;
for (const PrintInstance &instance : object->instances()) {
Points copy_points = object_hull.hull.points;
for (Point &pt : copy_points)
pt += instance.shift;
append(occupied_points, copy_points);
}
object->m_skirt.reverse();
auto append_brim_points = [&occupied_points](const ExPolygons& areas) {
for (const ExPolygon& area : areas)
append(occupied_points, area.contour.points);
};
if (auto it = m_objectBrimAreas.find(object->id()); it != m_objectBrimAreas.end())
append_brim_points(it->second);
if (auto it = m_supportBrimAreas.find(object->id()); it != m_supportBrimAreas.end())
append_brim_points(it->second);
if (occupied_points.size() < 3)
continue;
// Orca: include the object's brim/support-brim footprint before checking skirt collisions.
group_items.push_back({ std::move(occupied_points), object->id(), true });
}
// Orca: the wipe tower contributes occupied area, but does not emit a skirt by itself.
Points wipe_tower_points = this->first_layer_wipe_tower_corners();
if (wipe_tower_points.size() >= 3)
group_items.push_back({ std::move(wipe_tower_points), ObjectID(), false });
std::vector<size_t> parent(group_items.size());
std::iota(parent.begin(), parent.end(), 0);
// Orca: union-find keeps collision merging local without repeatedly rebuilding item lists.
auto find_parent = [&parent](size_t idx) {
while (parent[idx] != idx) {
parent[idx] = parent[parent[idx]];
idx = parent[idx];
}
return idx;
};
auto unite = [&parent, &find_parent](size_t a, size_t b) {
a = find_parent(a);
b = find_parent(b);
if (a != b)
parent[b] = a;
};
// Orca: combined skirt is the same grouping model with all items forced into one group.
if (m_config.skirt_type == stCombined && !group_items.empty())
for (size_t i = 1; i < group_items.size(); ++i)
unite(0, i);
auto build_grouped_points = [&]() {
struct GroupData {
Points points;
std::vector<ObjectID> object_ids;
bool emits_skirt = false;
};
std::map<size_t, GroupData> grouped;
for (size_t i = 0; i < group_items.size(); ++i) {
GroupData& group = grouped[find_parent(i)];
append(group.points, group_items[i].occupied_points);
if (group_items[i].object_id.valid())
group.object_ids.push_back(group_items[i].object_id);
group.emits_skirt = group.emits_skirt || group_items[i].emits_skirt;
}
return grouped;
};
bool groups_changed = m_config.skirt_type == stPerObject;
while (groups_changed) {
groups_changed = false;
auto grouped_points = build_grouped_points();
std::vector<std::pair<size_t, Polygon>> group_envelopes;
for (const auto& [root, group] : grouped_points) {
if (group.points.size() < 3)
continue;
// Orca: emitting groups are expanded to their final skirt reach; obstacle groups are not.
Polygon envelope = Geometry::convex_hull(group.points);
if (group.emits_skirt) {
// Orca: merge groups when a skirt envelope intersects another group or obstacle.
Polygons envelopes = offset(envelope, grouping_offset, ClipperLib::jtRound, float(scale_(0.1)));
if (envelopes.empty())
continue;
envelope = std::move(envelopes.front());
}
group_envelopes.emplace_back(root, std::move(envelope));
}
for (size_t i = 0; i < group_envelopes.size(); ++i) {
for (size_t j = i + 1; j < group_envelopes.size(); ++j) {
const size_t root_i = find_parent(group_envelopes[i].first);
const size_t root_j = find_parent(group_envelopes[j].first);
if (root_i != root_j && !intersection(group_envelopes[i].second, group_envelopes[j].second).empty()) {
unite(root_i, root_j);
groups_changed = true;
}
}
}
}
auto make_group_brims = [this](const std::vector<ObjectID>& group_object_ids) {
std::vector<SkirtBrimGroup::Brim> brims;
std::vector<ObjectID> brim_object_ids;
for (ObjectID object_id : group_object_ids) {
const auto brim_it = m_brimMap.find(object_id);
if (brim_it != m_brimMap.end() && !brim_it->second.empty())
brim_object_ids.push_back(object_id);
}
const bool global_combined_brim = m_config.combine_brims && m_config.skirt_type != stPerObject && m_brimMap.size() == 1;
auto brim_owner_ids = [&group_object_ids, global_combined_brim](const std::vector<ObjectID>& object_ids) {
return global_combined_brim ? group_object_ids : object_ids;
};
const bool combine_group_brims = m_config.combine_brims && brim_object_ids.size() > 1;
if (!combine_group_brims) {
for (ObjectID object_id : brim_object_ids)
brims.push_back({ m_brimMap.at(object_id), brim_owner_ids({ object_id }) });
return brims;
}
std::vector<size_t> brim_parent(brim_object_ids.size());
std::iota(brim_parent.begin(), brim_parent.end(), 0);
auto find_brim_parent = [&brim_parent](size_t idx) {
while (brim_parent[idx] != idx) {
brim_parent[idx] = brim_parent[brim_parent[idx]];
idx = brim_parent[idx];
}
return idx;
};
auto unite_brims = [&brim_parent, &find_brim_parent](size_t a, size_t b) {
a = find_brim_parent(a);
b = find_brim_parent(b);
if (a != b)
brim_parent[b] = a;
};
const coord_t brim_contact_distance = coord_t(brim_flow().scaled_spacing() * 2.);
for (size_t i = 0; i < brim_object_ids.size(); ++i) {
const auto area_i = m_objectBrimAreas.find(brim_object_ids[i]);
if (area_i == m_objectBrimAreas.end())
continue;
for (size_t j = i + 1; j < brim_object_ids.size(); ++j) {
const auto area_j = m_objectBrimAreas.find(brim_object_ids[j]);
if (area_j != m_objectBrimAreas.end() &&
!intersection_ex(offset_ex(area_i->second, brim_contact_distance, jtRound, SCALED_RESOLUTION), area_j->second).empty())
unite_brims(i, j);
}
}
std::map<size_t, std::vector<ObjectID>> combined_brim_ids;
for (size_t i = 0; i < brim_object_ids.size(); ++i)
combined_brim_ids[find_brim_parent(i)].push_back(brim_object_ids[i]);
for (const auto& [_, object_ids] : combined_brim_ids) {
if (object_ids.size() == 1) {
brims.push_back({ m_brimMap.at(object_ids.front()), brim_owner_ids(object_ids) });
continue;
}
ExPolygons combined_area;
for (ObjectID object_id : object_ids)
expolygons_append(combined_area, m_objectBrimAreas.at(object_id));
combined_area = union_ex(combined_area);
const float scaled_resolution = float(scaled(m_config.resolution.value));
const float brim_cleanup_delta = std::max(scaled_resolution, float(SCALED_EPSILON));
combined_area = offset2_ex(combined_area, brim_cleanup_delta, -brim_cleanup_delta, jtRound, scaled_resolution);
Polygons islands_area;
brims.push_back({ makeBrimInfillFromPlateCoordinates(combined_area, *this, islands_area), object_ids });
}
return brims;
};
auto grouped_points = build_grouped_points();
for (auto& [_, group] : grouped_points) {
if (!group.emits_skirt || group.points.size() < 3)
continue;
if (generate_skirt && m_config.skirt_type == stPerObject && group.object_ids.size() > 1)
m_has_shared_per_object_skirt = true;
// Orca: after merging, use the occupied outline directly; do not add skirt distance twice.
ExtrusionEntityCollection group_skirt;
if (generate_skirt)
append_skirt_loops_for_hull(Geometry::convex_hull(group.points), group_skirt, true);
std::vector<SkirtBrimGroup::Brim> group_brims = make_group_brims(group.object_ids);
if (!group_skirt.empty()) {
group_skirt.reverse();
// Orca: keep m_skirt as a flattened compatibility mirror for preview/extents.
m_skirt.append(group_skirt.entities);
}
if (!group_skirt.empty() || !group_brims.empty())
m_skirt_brim_groups.push_back({ std::move(group_skirt), std::move(group.object_ids), std::move(group_brims) });
}
}
}
@@ -3668,7 +3832,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
{
if (config().skirt_loops == 0 || config().skirt_type != stPerObject)
if (config().skirt_loops == 0 || config().skirt_type != stPerObject || m_objects.empty())
return std::make_tuple(0, 0);
float max_nozzle_diameter = *std::max_element(m_config.nozzle_diameter.values.begin(), m_config.nozzle_diameter.values.end());