Fix wipe tower placed outside bed boundary on first slice (#12777)
Some checks are pending
Build all / build_linux (push) Waiting to run
Build all / build_windows (push) Waiting to run
Build all / build_macos_arch (arm64) (push) Waiting to run
Build all / build_macos_arch (x86_64) (push) Waiting to run
Build all / Build macOS Universal (push) Blocked by required conditions
Build all / Unit Tests (push) Blocked by required conditions
Build all / Flatpak (push) Waiting to run

# Description

The wipe tower config position (wipe_tower_x/y) could be outside the plate boundary (e.g. default y=250 on a 200mm printer). No constraint was applied at slice time, so the tower was generated out-of-bounds.

issue reported in #12731

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the changes made in this PR.
-->
This commit is contained in:
SoftFever 2026-03-15 17:41:07 +08:00 committed by GitHub
commit 494601eea5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 22 additions and 1 deletions

View file

@ -2835,6 +2835,12 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config;
float x = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->get_at(plate_id);
float y = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->get_at(plate_id);
// Helper: persist corrected wipe tower position to config so the next slice uses valid coords.
auto persist_wipe_tower_pos = [&](float nx, float ny) {
ConfigOptionFloat cx(nx), cy(ny);
proj_cfg.option<ConfigOptionFloats>("wipe_tower_x")->set_at(&cx, plate_id, 0);
proj_cfg.option<ConfigOptionFloats>("wipe_tower_y")->set_at(&cy, plate_id, 0);
};
float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_tower_width"))->value;
float a = dynamic_cast<const ConfigOptionFloat*>(proj_cfg.option("wipe_tower_rotation_angle"))->value;
@ -2884,6 +2890,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
_set_warning_notification(EWarning::PreviewPrimeTowerOutside, true);
x = new_x;
y = new_y;
// Persist the correction to config so the next slice uses the valid position
persist_wipe_tower_pos(new_x, new_y);
}
@ -2906,7 +2914,13 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_id)->get_build_volume(true);
BoundingBox plate_bbox2d = BoundingBox(scaled(Vec2f(plate_bbox.min[0], plate_bbox.min[1])), scaled(Vec2f(plate_bbox.max[0], plate_bbox.max[1])));
Vec2f offset = WipeTower::move_box_inside_box(tower_bottom_bbox, plate_bbox2d, scaled(margin));
int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1),
// move_box_inside_box returns mm (already unscaled); apply directly.
// If the actual brim polygon is outside bounds, persist the correction to config.
float display_x = x + offset[0];
float display_y = y + offset[1];
if (offset.norm() > float(EPSILON))
persist_wipe_tower_pos(display_x, display_y);
int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, display_x + plate_origin(0), display_y + plate_origin(1),
current_print->wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh,
current_print->wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh,
true,a,/*!print->is_step_done(psWipeTower)*/ true, m_initialized);

View file

@ -4074,6 +4074,13 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx)
wt_x_opt = ConfigOptionFloat(I3_WIPE_TOWER_DEFAULT_X_POS);
wt_y_opt = ConfigOptionFloat(I3_WIPE_TOWER_DEFAULT_Y_POS);
}
// Clamp default position to fit within the actual plate dimensions so the wipe tower
// doesn't start outside the bed for printers smaller than the hardcoded defaults.
const double wt_default_margin = 2.;
const double wt_estimated_width = 60.; // conservative estimate matching prime_tower_width default
const double wt_estimated_depth = 20.; // conservative depth estimate
wt_x_opt.value = std::max(wt_default_margin, std::min(wt_x_opt.value, m_plate_width - wt_estimated_width - wt_default_margin));
wt_y_opt.value = std::max(wt_default_margin, std::min(wt_y_opt.value, m_plate_depth - wt_estimated_depth - wt_default_margin));
dynamic_cast<ConfigOptionFloats *>(proj_cfg.option("wipe_tower_x"))->set_at(&wt_x_opt, plate_idx, 0);
dynamic_cast<ConfigOptionFloats *>(proj_cfg.option("wipe_tower_y"))->set_at(&wt_y_opt, plate_idx, 0);
}