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

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();
};