feat(orient): auto-orient the largest overhang toward the cooling fan

New printer option fan_direction (undefine/left/right/both, default
undefine) declares which side the auxiliary part-cooling airflow comes
from. When set and the printer has an auxiliary fan, auto-orient adds a
yaw rotation so the dominant overhang area faces the airflow, and newly
added primitive shapes are pre-oriented the same way (except the Cube,
whose axis-aligned bounding box the pressure-advance pattern calibration
depends on).

- FanDirection enum + fan_direction printer option (Accessory group,
  enabled only with auxiliary_fan)
- orient engine: weighted overhang areas per candidate, yaw-direction
  search, vertical rotation applied on top of the primary orientation;
  the cooling weights are taken from the candidate actually chosen,
  including the flat-bottom tie-break
- orient_for_cooling() for primitive placement
- set fan_direction=left on H2C/H2D/H2D Pro/X1/X1E/P1S 0.4 profiles
  (X1C/H2S/P2S/X2D/Qidi X-Max 4 already carried the key, which now
  takes effect)

With fan_direction unset or no auxiliary fan the vertical rotation stays
identity and auto-orient results are unchanged; slicing and g-code are
never affected.
This commit is contained in:
SoftFever
2026-07-10 16:21:48 +08:00
parent 93a81179a1
commit b12aad6a8c
15 changed files with 283 additions and 19 deletions
+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
+1 -1
View File
@@ -1390,7 +1390,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","cooling_filter_enabled","printer_structure","farthest_point_timelapse",
"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","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",
+23
View File
@@ -512,6 +512,14 @@ static t_config_enum_values s_keys_map_NozzleType {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(NozzleType)
static t_config_enum_values s_keys_map_FanDirection {
{ "undefine", int(FanDirection::fdUndefine) },
{ "left", int(FanDirection::fdLeft) },
{ "right", int(FanDirection::fdRight) },
{ "both", int(FanDirection::fdBoth) }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FanDirection)
static t_config_enum_values s_keys_map_PrinterStructure {
{"undefine", int(PrinterStructure::psUndefine)},
{"corexy", int(PrinterStructure::psCoreXY)},
@@ -3997,6 +4005,21 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("fan_direction", coEnum);
def->label = L("Fan direction");
def->tooltip = L("Cooling fan direction of the printer");
def->enum_keys_map = &ConfigOptionEnum<FanDirection>::get_enum_values();
def->enum_values.push_back("undefine");
def->enum_values.push_back("left");
def->enum_values.push_back("right");
def->enum_values.push_back("both");
def->enum_labels.push_back(L("Undefined"));
def->enum_labels.push_back(L("Left"));
def->enum_labels.push_back(L("Right"));
def->enum_labels.push_back(L("Both"));
def->mode = comDevelop;
def->set_default_value(new ConfigOptionEnum<FanDirection>(fdUndefine));
def = this->add("fan_speedup_time", coFloat);
// Label is set in Tab.cpp in the Line object.
//def->label = L("Fan speed-up time");
+8
View File
@@ -362,6 +362,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"},
@@ -1494,6 +1501,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionEnumsGenericNullable,nozzle_type))
((ConfigOptionInt, nozzle_hrc))
((ConfigOptionBool, auxiliary_fan))
((ConfigOptionEnum<FanDirection>, fan_direction))
((ConfigOptionBool, support_air_filtration))
((ConfigOptionBool, cooling_filter_enabled))
((ConfigOptionEnum<PrinterStructure>,printer_structure))
+10
View File
@@ -38,6 +38,7 @@
#include "slic3r/Utils/FixModelByCgal.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Orient.hpp"
#include "libslic3r/PrintConfig.hpp"
#ifdef __WXMSW__
@@ -2560,6 +2561,15 @@ void ObjectList::load_shape_object(const std::string &type_name)
// Create mesh
BoundingBoxf3 bb;
TriangleMesh mesh = create_mesh(type_name, bb);
// Rotate the largest overhang area toward the part cooling fan so new shapes start in a
// cooling friendly orientation. Skip the plain cube: the pressure advance pattern
// calibration reuses it as an axis-aligned anchor and scales it by its bounding box,
// so its orientation must stay fixed.
const Slic3r::DynamicPrintConfig& full_config = wxGetApp().preset_bundle->full_config();
if (type_name != "Cube" && full_config.has("fan_direction") && full_config.has("auxiliary_fan")) {
FanDirection config_dir = full_config.option<ConfigOptionEnum<FanDirection>>("fan_direction")->value;
orientation::orient_for_cooling(mesh, config_dir);
}
// BBS: remove "Shape" prefix
load_mesh_object(mesh, _(type_name));
wxGetApp().mainframe->update_title();
+18 -1
View File
@@ -229,15 +229,32 @@ orientation::OrientMesh OrientJob::get_orient_mesh(ModelInstance* instance)
auto obj = instance->get_object();
om.name = obj->name;
om.mesh = obj->mesh(); // don't know the difference to obj->raw_mesh(). Both seem OK
const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config();
if (obj->config.has("support_threshold_angle"))
om.overhang_angle = obj->config.opt_int("support_threshold_angle");
else {
const Slic3r::DynamicPrintConfig& config = wxGetApp().preset_bundle->full_config();
om.overhang_angle = config.opt_int("support_threshold_angle");
}
if (config.has("fan_direction") && config.has("auxiliary_fan")) {
FanDirection config_dir = config.option<ConfigOptionEnum<FanDirection>>("fan_direction")->value;
if (config_dir == FanDirection::fdUndefine || !config.opt_bool("auxiliary_fan")) {
// no part cooling airflow to face, keep the orientation around the z axis unchanged
om.cooling_direction = {0, 0, 0};
} else if (config_dir == FanDirection::fdRight) {
// the part cooling airflow comes from the right side
om.cooling_direction = {1, 0, 0};
om.has_cooling_fan = true;
} else {
// the part cooling airflow comes from the left side, or from both sides
om.cooling_direction = {-1, 0, 0};
om.has_cooling_fan = true;
}
}
om.setter = [instance](const OrientMesh& p) {
instance->rotate(p.rotation_matrix);
instance->rotate(p.rotation_matrix_vertical);
instance->get_object()->invalidate_bounding_box();
instance->get_object()->ensure_on_bed();
};
+3
View File
@@ -4956,6 +4956,7 @@ void TabPrinter::build_fff()
optgroup->append_single_option_line("nozzle_type", "printer_basic_information_accessory#nozzle-type");
optgroup->append_single_option_line("nozzle_hrc", "printer_basic_information_accessory#nozzle-hrc");
optgroup->append_single_option_line("auxiliary_fan", "printer_basic_information_accessory#auxiliary-part-cooling-fan");
optgroup->append_single_option_line("fan_direction");
optgroup->append_single_option_line("support_chamber_temp_control", "printer_basic_information_accessory#support-controlling-chamber-temperature");
optgroup->append_single_option_line("support_air_filtration", "printer_basic_information_accessory#support-air-filtration");
@@ -5892,6 +5893,8 @@ void TabPrinter::toggle_options()
const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads");
toggle_line("parallel_printheads_count", support_parallel_printheads);
toggle_line("fan_direction", m_config->opt_bool("auxiliary_fan"));
}