Merge branch 'main' into feat/plugin-pages

This commit is contained in:
Ian Chua
2026-08-11 17:51:18 +08:00
54 changed files with 1541 additions and 469 deletions
+21 -43
View File
@@ -2871,6 +2871,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
}
if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) {
// The tower size estimate reads printer- and filament-scope keys, which the print preset
// does not carry; built once here rather than per plate.
const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config();
for (int plate_id = 0; plate_id < n_plates; plate_id++) {
// If print ByObject and there is only one object in the plate, the wipe tower is allowed to be generated.
PartPlate* part_plate = ppl.get_plate(plate_id);
@@ -2895,51 +2898,26 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
if (part_plate->get_objects_on_this_plate().empty()) continue;
float brim_width = print->wipe_tower_data(filaments_count).brim_width;
const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value);
Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value);
{
const float margin = WIPE_TOWER_MARGIN + brim_width;
BoundingBoxf3 plate_bbox = part_plate->get_bounding_box();
BoundingBoxf plate_bbox_2d(Vec2d(plate_bbox.min(0), plate_bbox.min(1)), Vec2d(plate_bbox.max(0), plate_bbox.max(1)));
const std::vector<Pointfs> &extruder_areas = part_plate->get_extruder_areas();
for (Pointfs points : extruder_areas) {
BoundingBoxf bboxf(points);
plate_bbox_2d.min = plate_bbox_2d.min(0) >= bboxf.min(0) ? plate_bbox_2d.min : bboxf.min;
plate_bbox_2d.max = plate_bbox_2d.max(0) <= bboxf.max(0) ? plate_bbox_2d.max : bboxf.max;
}
coordf_t plate_bbox_x_min_local_coord = plate_bbox_2d.min(0) - plate_origin(0);
coordf_t plate_bbox_x_max_local_coord = plate_bbox_2d.max(0) - plate_origin(0);
coordf_t plate_bbox_y_max_local_coord = plate_bbox_2d.max(1) - plate_origin(1);
if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) {
// update for wipe tower position
{
int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1),
(float) wipe_tower_size(0), (float) wipe_tower_size(1), (float) wipe_tower_size(2),
a,
/*!print->is_step_done(psWipeTower)*/ true, brim_width);
int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id];
if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new;
}
} else {
const float margin = 2.f;
auto tower_bottom = current_print->wipe_tower_data().wipe_tower_mesh_data->bottom;
tower_bottom.translate(scaled(Vec2d{x, y}));
tower_bottom.translate(scaled(Vec2d{plate_origin[0], plate_origin[1]}));
auto tower_bottom_bbox = get_extents(tower_bottom);
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),
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);
int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id];
if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new;
}
// The stored position is already clamped onto the bed, by
// set_default_wipe_tower_pos_for_plate and again on every drag.
if (!current_print->is_step_done(psWipeTower) || !current_print->wipe_tower_data().wipe_tower_mesh_data) {
// update for wipe tower position
int volume_idx_wipe_tower_new = m_volumes.load_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), y + plate_origin(1),
(float) wipe_tower_size(0), (float) wipe_tower_size(1), (float) wipe_tower_size(2),
a,
/*!print->is_step_done(psWipeTower)*/ true, brim_width);
int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id];
if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new;
} else {
int volume_idx_wipe_tower_new = m_volumes.load_real_wipe_tower_preview(1000 + plate_id, x + plate_origin(0), 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);
int volume_idx_wipe_tower_old = volume_idxs_wipe_tower_old[plate_id];
if (volume_idx_wipe_tower_old != -1) map_glvolume_old_to_new[volume_idx_wipe_tower_old] = volume_idx_wipe_tower_new;
}
}
}
+30 -6
View File
@@ -2262,6 +2262,12 @@ Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, con
}
double volume = wipe_volume * (extruder_count == 2 ? plate_extruder_size : (plate_extruder_size - 1));
if (extruder_count == 2) volume += filament_change_volume * (int) (plate_extruder_size / 2);
// Read from the passed plate config — m_print may not have been applied yet
// (fresh plates, CLI), in which case its PrintConfig still holds defaults.
const auto *purge_opt = config.option<ConfigOptionBool>("purge_in_prime_tower");
const auto *semm_opt = config.option<ConfigOptionBool>("single_extruder_multi_material");
const bool semm_flush = purge_opt && purge_opt->value && semm_opt && semm_opt->value;
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(config, plate_extruder_size);
if (use_rib_wall) {
depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || plate_extruder_size > 1) {
@@ -2274,7 +2280,9 @@ Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, con
}
}
else {
depth = volume/ (layer_height * w) *extra_spacing;
depth = volume / (layer_height * w);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush) depth *= extra_spacing;
if (need_wipe_tower || depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double)min_wipe_tower_depth, depth);
@@ -3337,6 +3345,11 @@ BoundingBoxf3 PartPlate::get_build_volume(bool use_share)
return plate_box;
}
Polygon PartPlate::get_shared_printable_polygon() const
{
return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas);
}
bool PartPlate::contains(const Vec3d& point) const
{
return m_bounding_box.contains(point);
@@ -4375,22 +4388,21 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini
f_volume_maps = wxGetApp().preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps);
}
DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps);
const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
float w = dynamic_cast<const ConfigOptionFloat *>(print_cfg.option("prime_tower_width"))->value;
float w = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_tower_width"))->value;
float v = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_volume"))->value;
bool enable_wrapping = false;
const ConfigOptionBool *wrapping_opt = dynamic_cast<const ConfigOptionBool *>(full_config.option("enable_wrapping_detection"));
if (wrapping_opt) enable_wrapping = wrapping_opt->value;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
Vec3d wipe_tower_size = part_plate->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, init_pos ? 2 : 0, false, enable_wrapping);
Vec3d wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, init_pos ? 2 : 0, false, enable_wrapping);
if (!init_pos && (is_approx(wipe_tower_size(0), 0.0) || is_approx(wipe_tower_size(1), 0.0))) {
wipe_tower_size = part_plate->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 2, false, enable_wrapping);
wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 2, false, enable_wrapping);
}
// Compute brim-aware margin: brim extends outward from tower position
float brim_width = 0.f;
const ConfigOptionFloat *brim_opt = print_cfg.option<ConfigOptionFloat>("prime_tower_brim_width");
const ConfigOptionFloat *brim_opt = full_config.option<ConfigOptionFloat>("prime_tower_brim_width");
if (brim_opt) {
brim_width = brim_opt->value;
if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z());
@@ -4412,6 +4424,18 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini
}
}
// The bounding box above still allows a corner a delta or hexagonal bed does not have, and the
// prime tower is validated against the real outline — pull it onto the bed before storing.
{
Polygons bed{part_plate->get_shared_printable_polygon()};
bed.front().translate(Point(-scaled(plate_origin.x()), -scaled(plate_origin.y()))); // into the frame x/y live in
const BoundingBox tower(Point::new_scale(x, y),
Point::new_scale(x + wipe_tower_size(0), y + wipe_tower_size(1)));
const Vec2f move = WipeTower::move_box_inside_polygon(tower, bed, scaled<coord_t>(margin));
x += move.x();
y += move.y();
}
ConfigOptionFloat wt_x_opt(x);
ConfigOptionFloat wt_y_opt(y);
dynamic_cast<ConfigOptionFloats *>(proj_cfg.option("wipe_tower_x"))->set_at(&wt_x_opt, plate_idx, 0);
+3
View File
@@ -425,6 +425,9 @@ public:
const BoundingBox get_bounding_box_crd();
BoundingBoxf3 get_plate_box() {return get_build_volume();}
BoundingBoxf3 get_build_volume(bool use_share = false);
// Polygon counterpart of get_build_volume(true), in scaled world coordinates. The bounding box
// that one returns hides the corners a non-rectangular bed does not have.
Polygon get_shared_printable_polygon() const;
const std::vector<BoundingBoxf3>& get_exclude_areas() { return m_exclude_bounding_box; }
+5 -5
View File
@@ -9501,7 +9501,7 @@ void Plater::priv::replace_all_with_stl()
return;
}
std::string status = _L("Replaced with 3D files from directory:\n").ToStdString() + out_path.string() + "\n\n";
wxString status = _L("Replaced with 3D files from directory:\n") + from_u8(out_path.string()) + "\n\n";
for (unsigned int idx : volume_idxs) {
const GLVolume* v = selection.get_volume(idx);
@@ -9521,13 +9521,13 @@ void Plater::priv::replace_all_with_stl()
std::string volume_name = volume->name;
if (new_path == input_path) {
status += boost::str(boost::format(_L("✖ Skipped %1%: same file.\n").ToStdString()) % volume_name);
status += wxString::Format(_L("✖ Skipped %s: same file.\n"), from_u8(volume_name));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " skipping replace volume : same filename " << new_path;
continue;
}
if (!fs::exists(new_path)) {
status += boost::str(boost::format(_L("✖ Skipped %1%: file does not exist.\n").ToStdString()) % volume_name);
status += wxString::Format(_L("✖ Skipped %s: file does not exist.\n"), from_u8(volume_name));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : filen does not exist " << new_path;
continue;
}
@@ -9535,12 +9535,12 @@ void Plater::priv::replace_all_with_stl()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " replacing volume : " << input_path << " with " << new_path;
if (!replace_volume_with_stl(object_idx, volume_idx, new_path, _u8L("Replace with 3D file"))) {
status += boost::str(boost::format(_L("✖ Skipped %1%: failed to replace.\n").ToStdString()) % volume_name);
status += wxString::Format(_L("✖ Skipped %s: failed to replace.\n"), from_u8(volume_name));
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " cannot replace volume : failed to replace with " << new_path;
continue;
}
status += boost::str(boost::format(_L("✔ Replaced %1%.\n").ToStdString()) % volume_name);
status += wxString::Format(_L("✔ Replaced %s.\n"), from_u8(volume_name));
}
// update 3D scene
+38 -5
View File
@@ -111,13 +111,46 @@ SavePresetDialog::Item::Item(Preset::Type type, const std::string &suffix, wxBox
sizer->Add(m_radio_group, 0, wxEXPAND | wxTOP | wxLEFT, BORDER_W);
if (parent->m_mode == comDevelop) {
m_detach_checkbox = new wxCheckBox(parent, wxID_ANY, _L("Detach from parent"));
sizer->Add(m_detach_checkbox, 0, wxALIGN_LEFT | wxALL, BORDER_W);
std::string inherits_str = sel_preset.inherits();
if (parent->m_mode == comDevelop && !inherits_str.empty()) {
wxBoxSizer *detach_sizer = new wxBoxSizer(wxHORIZONTAL);
auto detach_tooltip = _L("Copies all inherited values from the parent preset into this preset and removes the connection with the parent preset.");
auto detach_checkbox = new ::CheckBox(parent);
detach_checkbox->SetToolTip(detach_tooltip);
auto detach_label = new wxStaticText(parent, wxID_ANY, _L("Detach from parent"));
detach_label->SetFont(::Label::Body_14);
detach_label->SetForegroundColour(wxColour("#363636"));
detach_label->SetToolTip(detach_tooltip);
detach_sizer->Add(detach_checkbox, 0, wxALIGN_LEFT | wxLEFT, BORDER_W);
detach_sizer->Add(detach_label , 0, wxALIGN_CENTRE_VERTICAL | wxLEFT, FromDIP(5));
sizer->Add(detach_sizer, 0, wxEXPAND | wxTOP, BORDER_W);
sizer->AddSpacer(FromDIP(5));
auto parent_label = new wxStaticText(parent, wxID_ANY, inherits_str);
parent_label->SetFont(::Label::Body_12);
parent_label->SetForegroundColour(wxColour("#6B6B6B"));
parent_label->SetToolTip(_L("Parent preset"));
sizer->Add(parent_label, 0, wxEXPAND | wxLEFT, BORDER_W + FromDIP(24));
sizer->AddSpacer(FromDIP(5));
// Set initial state (unchecked by default)
m_detach_checkbox->SetValue(m_detach);
detach_checkbox->SetValue(m_detach);
// Bind the checkbox event to update the detach state for this item
m_detach_checkbox->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { m_detach = m_detach_checkbox->GetValue(); });
detach_checkbox->Bind(wxEVT_TOGGLEBUTTON, [this, detach_checkbox](wxCommandEvent&) { m_detach = detach_checkbox->GetValue(); });
auto on_toggle = [this, detach_checkbox]() {
detach_checkbox->SetValue(!detach_checkbox->GetValue());
wxCommandEvent ev(wxEVT_TOGGLEBUTTON, detach_checkbox->GetId());
ev.SetEventObject(detach_checkbox);
detach_checkbox->GetEventHandler()->ProcessEvent(ev);
};
detach_label->Bind(wxEVT_LEFT_DOWN, [on_toggle](wxMouseEvent& e) {if(!e.LeftDClick()) on_toggle();});
detach_label->Bind(wxEVT_LEFT_DCLICK, [on_toggle](wxMouseEvent& e) {on_toggle();});
}
m_radio_group->Bind(wxEVT_COMMAND_RADIOBOX_SELECTED, [this](wxCommandEvent &e) {
-1
View File
@@ -75,7 +75,6 @@ class SavePresetDialog : public DPIDialog
bool m_save_to_project {false};
RadioGroup* m_radio_group; // ORCA
bool m_detach{false};
wxCheckBox* m_detach_checkbox{nullptr};
void update();
};
+2 -15
View File
@@ -1270,9 +1270,7 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor
} else {
if (v.is_wipe_tower) {//in world cs
int plate_idx = v.object_idx() - 1000;
BoundingBoxf3 plate_bbox = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->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])));
Vec3d tower_size = v.bounding_box().size();
const Polygons bed_polys{wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->get_shared_printable_polygon()};
Vec3d tower_origin = m_cache.volumes_data[i].get_volume_position();
Vec3d actual_displacement = displacement;
bool show_read_wipe_tower = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx)->fff_print()->is_step_done(psWipeTower);
@@ -1287,18 +1285,7 @@ void Selection::translate(const Vec3d &displacement, TransformationType transfor
BoundingBoxf3 tower_bbox = v.bounding_box();
tower_bbox.translate(actual_displacement + tower_origin);
BoundingBox tower_bbox2d = BoundingBox(scaled(Vec2f(tower_bbox.min[0], tower_bbox.min[1])), scaled(Vec2f(tower_bbox.max[0], tower_bbox.max[1])));
Vec2f offset = WipeTower::move_box_inside_box(tower_bbox2d, plate_bbox2d,scaled(margin));
//if (tower_origin(0) + actual_displacement(0) - margin < plate_bbox.min(0)) {
// actual_displacement(0) = plate_bbox.min(0) - tower_origin(0) + margin;
//} else if (tower_origin(0) + actual_displacement(0) + tower_size(0) + margin > plate_bbox.max(0)) {
// actual_displacement(0) = plate_bbox.max(0) - tower_origin(0) - tower_size(0) - margin;
//}
//if (tower_origin(1) + actual_displacement(1) - margin < plate_bbox.min(1)) {
// actual_displacement(1) = plate_bbox.min(1) - tower_origin(1) + margin;
//} else if (tower_origin(1) + actual_displacement(1) + tower_size(1) + margin > plate_bbox.max(1)) {
// actual_displacement(1) = plate_bbox.max(1) - tower_origin(1) - tower_size(1) - margin;
//}
const Vec2f offset = WipeTower::move_box_inside_polygon(tower_bbox2d, bed_polys, scaled(margin));
actual_displacement += Vec3d(offset[0], offset[1],0);
v.set_volume_offset(m_cache.volumes_data[i].get_volume_position() + actual_displacement);
}
+2
View File
@@ -5627,6 +5627,7 @@ if (is_marlin_flavor)
optgroup->append_single_option_line("purge_in_prime_tower", "printer_multimaterial_wipe_tower#purge-in-prime-tower");
optgroup->append_single_option_line("enable_filament_ramming", "printer_multimaterial_wipe_tower#enable-filament-ramming");
optgroup->append_single_option_line("tool_change_on_wipe_tower", "printer_multimaterial_wipe_tower#tool-change-on-wipe-tower");
optgroup->append_single_option_line("wait_for_temp_on_wipe_tower", "printer_multimaterial_wipe_tower#wait-for-temperature-on-wipe-tower");
optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings");
@@ -6160,6 +6161,7 @@ void TabPrinter::toggle_options()
// so the option is irrelevant there.
const size_t extruders_count = m_config->option<ConfigOptionFloats>("nozzle_diameter")->size();
toggle_option("tool_change_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1);
toggle_option("wait_for_temp_on_wipe_tower", !bSEMM && supports_wipe_tower_2 && extruders_count > 1);
}
wxString extruder_number;
long val = 1;