PrusaSlicer-NonPlainar/src/slic3r/GUI/GUI_ObjectManipulation.cpp
enricoturri1966 116f928903 Tech ENABLE_WORLD_COORDINATE - 1st installment
1) Added combo to select world/local coordinate to part manipulator in sidebar

2) Gizmo move oriented in dependence of the selected coordinate system

3) Sidebar hints for position oriented in dependence of the selected coordinate system
2021-10-08 14:32:02 +02:00

1210 lines
46 KiB
C++

#include "GUI_ObjectManipulation.hpp"
#include "I18N.hpp"
#include "BitmapComboBox.hpp"
#include "GLCanvas3D.hpp"
#include "OptionsGroup.hpp"
#include "GUI_App.hpp"
#include "wxExtensions.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Geometry.hpp"
#include "Selection.hpp"
#include "Plater.hpp"
#include "MainFrame.hpp"
#include "MsgDialog.hpp"
#include <boost/algorithm/string.hpp>
#include "slic3r/Utils/FixModelByWin10.hpp"
namespace Slic3r
{
namespace GUI
{
const double ObjectManipulation::in_to_mm = 25.4;
const double ObjectManipulation::mm_to_in = 0.0393700787;
// Helper function to be used by drop to bed button. Returns lowest point of this
// volume in world coordinate system.
static double get_volume_min_z(const GLVolume& volume)
{
return volume.transformed_convex_hull_bounding_box().min.z();
}
static choice_ctrl* create_word_local_combo(wxWindow *parent)
{
wxSize size(15 * wxGetApp().em_unit(), -1);
choice_ctrl* temp = nullptr;
#ifdef __WXOSX__
/* wxBitmapComboBox with wxCB_READONLY style return NULL for GetTextCtrl(),
* so ToolTip doesn't shown.
* Next workaround helps to solve this problem
*/
temp = new wxBitmapComboBox();
temp->SetTextCtrlStyle(wxTE_READONLY);
temp->Create(parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr);
#else
temp = new choice_ctrl(parent, wxID_ANY, wxString(""), wxDefaultPosition, size, 0, nullptr, wxCB_READONLY | wxBORDER_SIMPLE);
#endif //__WXOSX__
temp->SetFont(Slic3r::GUI::wxGetApp().normal_font());
if (!wxOSX) temp->SetBackgroundStyle(wxBG_STYLE_PAINT);
temp->Append(_L("World coordinates"));
temp->Append(_L("Local coordinates"));
temp->SetSelection(0);
temp->SetValue(temp->GetString(0));
temp->SetToolTip(_L("Select coordinate space, in which the transformation will be performed."));
return temp;
}
void msw_rescale_word_local_combo(choice_ctrl* combo)
{
#ifdef __WXOSX__
const wxString selection = combo->GetString(combo->GetSelection());
/* To correct scaling (set new controll size) of a wxBitmapCombobox
* we need to refill control with new bitmaps. So, in our case :
* 1. clear control
* 2. add content
* 3. add scaled "empty" bitmap to the at least one item
*/
combo->Clear();
wxSize size(wxDefaultSize);
size.SetWidth(15 * wxGetApp().em_unit());
// Set rescaled min height to correct layout
combo->SetMinSize(wxSize(-1, int(1.5f*combo->GetFont().GetPixelSize().y + 0.5f)));
// Set rescaled size
combo->SetSize(size);
combo->Append(_L("World coordinates"));
combo->Append(_L("Local coordinates"));
combo->SetValue(selection);
#else
#ifdef _WIN32
combo->Rescale();
#endif
combo->SetMinSize(wxSize(15 * wxGetApp().em_unit(), -1));
#endif
}
static void set_font_and_background_style(wxWindow* win, const wxFont& font)
{
win->SetFont(font);
win->SetBackgroundStyle(wxBG_STYLE_PAINT);
}
static const wxString axes_color_text[] = { "#990000", "#009900", "#000099" };
static const wxString axes_color_back[] = { "#f5dcdc", "#dcf5dc", "#dcdcf5" };
ObjectManipulation::ObjectManipulation(wxWindow* parent) :
OG_Settings(parent, true)
{
m_imperial_units = wxGetApp().app_config->get("use_inches") == "1";
m_use_colors = wxGetApp().app_config->get("color_mapinulation_panel") == "1";
m_manifold_warning_bmp = ScalableBitmap(parent, "exclamation");
// Load bitmaps to be used for the mirroring buttons:
m_mirror_bitmap_on = ScalableBitmap(parent, "mirroring_on");
m_mirror_bitmap_off = ScalableBitmap(parent, "mirroring_off");
m_mirror_bitmap_hidden = ScalableBitmap(parent, "mirroring_transparent.png");
const int border = wxOSX ? 0 : 4;
const int em = wxGetApp().em_unit();
m_main_grid_sizer = new wxFlexGridSizer(2, 3, 3); // "Name/label", "String name / Editors"
m_main_grid_sizer->SetFlexibleDirection(wxBOTH);
// Add "Name" label with warning icon
auto sizer = new wxBoxSizer(wxHORIZONTAL);
m_fix_throught_netfab_bitmap = new wxStaticBitmap(parent, wxID_ANY, wxNullBitmap);
if (is_windows10())
m_fix_throught_netfab_bitmap->Bind(wxEVT_CONTEXT_MENU, [this](wxCommandEvent& e)
{
// if object/sub-object has no errors
if (m_fix_throught_netfab_bitmap->GetBitmap().GetRefData() == wxNullBitmap.GetRefData())
return;
wxGetApp().obj_list()->fix_through_netfabb();
update_warning_icon_state(wxGetApp().obj_list()->get_mesh_errors_info());
});
sizer->Add(m_fix_throught_netfab_bitmap);
auto name_label = new wxStaticText(m_parent, wxID_ANY, _L("Name")+":");
set_font_and_background_style(name_label, wxGetApp().normal_font());
name_label->SetToolTip(_L("Object name"));
sizer->Add(name_label);
m_main_grid_sizer->Add(sizer);
// Add name of the item
const wxSize name_size = wxSize(20 * em, wxDefaultCoord);
m_item_name = new wxStaticText(m_parent, wxID_ANY, "", wxDefaultPosition, name_size, wxST_ELLIPSIZE_MIDDLE);
set_font_and_background_style(m_item_name, wxGetApp().bold_font());
m_main_grid_sizer->Add(m_item_name, 0, wxEXPAND);
// Add labels grid sizer
m_labels_grid_sizer = new wxFlexGridSizer(1, 3, 3); // "Name/label", "String name / Editors"
m_labels_grid_sizer->SetFlexibleDirection(wxBOTH);
// Add world local combobox
m_word_local_combo = create_word_local_combo(parent);
m_word_local_combo->Bind(wxEVT_COMBOBOX, ([this](wxCommandEvent& evt) {
this->set_world_coordinates(evt.GetSelection() != 1);
}), m_word_local_combo->GetId());
// Small trick to correct layouting in different view_mode :
// Show empty string of a same height as a m_word_local_combo, when m_word_local_combo is hidden
m_word_local_combo_sizer = new wxBoxSizer(wxHORIZONTAL);
m_empty_str = new wxStaticText(parent, wxID_ANY, "");
m_word_local_combo_sizer->Add(m_word_local_combo);
m_word_local_combo_sizer->Add(m_empty_str);
m_word_local_combo_sizer->SetMinSize(wxSize(-1, m_word_local_combo->GetBestHeight(-1)));
m_labels_grid_sizer->Add(m_word_local_combo_sizer);
// Text trick to grid sizer layout:
// Height of labels should be equivalent to the edit boxes
int height = wxTextCtrl(parent, wxID_ANY, "Br").GetBestHeight(-1);
#ifdef __WXGTK__
// On Linux button with bitmap has bigger height then regular button or regular TextCtrl
// It can cause a wrong alignment on show/hide of a reset buttons
const int bmp_btn_height = ScalableButton(parent, wxID_ANY, "undo") .GetBestHeight(-1);
if (bmp_btn_height > height)
height = bmp_btn_height;
#endif //__WXGTK__
auto add_label = [this, height](wxStaticText** label, const std::string& name, wxSizer* reciver = nullptr)
{
*label = new wxStaticText(m_parent, wxID_ANY, _(name) + ":");
set_font_and_background_style(*label, wxGetApp().normal_font());
wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->SetMinSize(wxSize(-1, height));
sizer->Add(*label, 0, wxALIGN_CENTER_VERTICAL);
if (reciver)
reciver->Add(sizer);
else
m_labels_grid_sizer->Add(sizer);
m_rescalable_sizers.push_back(sizer);
};
// Add labels
add_label(&m_move_Label, L("Position"));
add_label(&m_rotate_Label, L("Rotation"));
// additional sizer for lock and labels "Scale" & "Size"
sizer = new wxBoxSizer(wxHORIZONTAL);
m_lock_bnt = new LockButton(parent, wxID_ANY);
m_lock_bnt->Bind(wxEVT_BUTTON, [this](wxCommandEvent& event) {
event.Skip();
wxTheApp->CallAfter([this]() { set_uniform_scaling(m_lock_bnt->IsLocked()); });
});
sizer->Add(m_lock_bnt, 0, wxALIGN_CENTER_VERTICAL);
auto v_sizer = new wxGridSizer(1, 3, 3);
add_label(&m_scale_Label, L("Scale"), v_sizer);
wxStaticText* size_Label {nullptr};
add_label(&size_Label, L("Size"), v_sizer);
if (wxOSX) set_font_and_background_style(size_Label, wxGetApp().normal_font());
sizer->Add(v_sizer, 0, wxLEFT, border);
m_labels_grid_sizer->Add(sizer);
m_main_grid_sizer->Add(m_labels_grid_sizer, 0, wxEXPAND);
// Add editors grid sizer
wxFlexGridSizer* editors_grid_sizer = new wxFlexGridSizer(5, 3, 3); // "Name/label", "String name / Editors"
editors_grid_sizer->SetFlexibleDirection(wxBOTH);
// Add Axes labels with icons
static const char axes[] = { 'X', 'Y', 'Z' };
// std::vector<wxString> axes_color = {"#EE0000", "#00EE00", "#0000EE"};
for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++) {
const char label = axes[axis_idx];
wxStaticText* axis_name = new wxStaticText(m_parent, wxID_ANY, wxString(label));
set_font_and_background_style(axis_name, wxGetApp().bold_font());
//if (m_use_colors)
// axis_name->SetForegroundColour(wxColour(axes_color_text[axis_idx]));
sizer = new wxBoxSizer(wxHORIZONTAL);
// Under OSX we use font, smaller than default font, so
// there is a next trick for an equivalent layout of coordinates combobox and axes labels in they own sizers
if (wxOSX)
sizer->SetMinSize(-1, m_word_local_combo->GetBestHeight(-1));
sizer->Add(axis_name, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, border);
// We will add a button to toggle mirroring to each axis:
auto btn = new ScalableButton(parent, wxID_ANY, "mirroring_off", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER | wxTRANSPARENT_WINDOW);
btn->SetToolTip(wxString::Format(_L("Toggle %c axis mirroring"), (int)label));
btn->SetBitmapDisabled_(m_mirror_bitmap_hidden);
m_mirror_buttons[axis_idx].first = btn;
m_mirror_buttons[axis_idx].second = mbShown;
sizer->AddStretchSpacer(2);
sizer->Add(btn, 0, wxALIGN_CENTER_VERTICAL);
btn->Bind(wxEVT_BUTTON, [this, axis_idx](wxCommandEvent&) {
Axis axis = (Axis)(axis_idx + X);
if (m_mirror_buttons[axis_idx].second == mbHidden)
return;
GLCanvas3D* canvas = wxGetApp().plater()->canvas3D();
Selection& selection = canvas->get_selection();
if (selection.is_single_volume() || selection.is_single_modifier()) {
GLVolume* volume = const_cast<GLVolume*>(selection.get_volume(*selection.get_volume_idxs().begin()));
volume->set_volume_mirror(axis, -volume->get_volume_mirror(axis));
}
else if (selection.is_single_full_instance()) {
for (unsigned int idx : selection.get_volume_idxs()) {
GLVolume* volume = const_cast<GLVolume*>(selection.get_volume(idx));
volume->set_instance_mirror(axis, -volume->get_instance_mirror(axis));
}
}
else
return;
// Update mirroring at the GLVolumes.
selection.synchronize_unselected_instances(Selection::SYNC_ROTATION_GENERAL);
selection.synchronize_unselected_volumes();
// Copy mirroring values from GLVolumes into Model (ModelInstance / ModelVolume), trigger background processing.
canvas->do_mirror(L("Set Mirror"));
UpdateAndShow(true);
});
editors_grid_sizer->Add(sizer, 0, wxALIGN_CENTER_HORIZONTAL);
}
editors_grid_sizer->AddStretchSpacer(1);
editors_grid_sizer->AddStretchSpacer(1);
// add EditBoxes
auto add_edit_boxes = [this, editors_grid_sizer](const std::string& opt_key, int axis)
{
ManipulationEditor* editor = new ManipulationEditor(this, opt_key, axis);
m_editors.push_back(editor);
editors_grid_sizer->Add(editor, 0, wxALIGN_CENTER_VERTICAL);
};
// add Units
auto add_unit_text = [this, parent, editors_grid_sizer, height](std::string unit, wxStaticText** unit_text)
{
*unit_text = new wxStaticText(parent, wxID_ANY, _(unit));
set_font_and_background_style(*unit_text, wxGetApp().normal_font());
// Unit text should be the same height as labels
wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
sizer->SetMinSize(wxSize(-1, height));
sizer->Add(*unit_text, 0, wxALIGN_CENTER_VERTICAL);
editors_grid_sizer->Add(sizer);
m_rescalable_sizers.push_back(sizer);
};
for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++)
add_edit_boxes("position", axis_idx);
add_unit_text(m_imperial_units ? L("in") : L("mm"), &m_position_unit);
// Add drop to bed button
m_drop_to_bed_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "drop_to_bed"));
m_drop_to_bed_button->SetToolTip(_L("Drop to bed"));
m_drop_to_bed_button->Bind(wxEVT_BUTTON, [=](wxCommandEvent& e) {
// ???
GLCanvas3D* canvas = wxGetApp().plater()->canvas3D();
Selection& selection = canvas->get_selection();
if (selection.is_single_volume() || selection.is_single_modifier()) {
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
const Geometry::Transformation& instance_trafo = volume->get_instance_transformation();
const Vec3d diff = m_cache.position - instance_trafo.get_matrix(true).inverse() * Vec3d(0., 0., get_volume_min_z(*volume));
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Drop to bed"));
change_position_value(0, diff.x());
change_position_value(1, diff.y());
change_position_value(2, diff.z());
}
else if (selection.is_single_full_instance()) {
const ModelObjectPtrs& objects = wxGetApp().model().objects;
const int idx = selection.get_object_idx();
if (0 <= idx && idx < static_cast<int>(objects.size())) {
const ModelObject* mo = wxGetApp().model().objects[idx];
const double min_z = mo->bounding_box().min.z();
if (std::abs(min_z) > SINKING_Z_THRESHOLD) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Drop to bed"));
change_position_value(2, m_cache.position.z() - min_z);
}
}
}
});
editors_grid_sizer->Add(m_drop_to_bed_button);
for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++)
add_edit_boxes("rotation", axis_idx);
wxStaticText* rotation_unit{ nullptr };
add_unit_text("°", &rotation_unit);
// Add reset rotation button
m_reset_rotation_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "undo"));
m_reset_rotation_button->SetToolTip(_L("Reset rotation"));
m_reset_rotation_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
GLCanvas3D* canvas = wxGetApp().plater()->canvas3D();
Selection& selection = canvas->get_selection();
if (selection.is_single_volume() || selection.is_single_modifier()) {
GLVolume* volume = const_cast<GLVolume*>(selection.get_volume(*selection.get_volume_idxs().begin()));
volume->set_volume_rotation(Vec3d::Zero());
}
else if (selection.is_single_full_instance()) {
for (unsigned int idx : selection.get_volume_idxs()) {
GLVolume* volume = const_cast<GLVolume*>(selection.get_volume(idx));
volume->set_instance_rotation(Vec3d::Zero());
}
}
else
return;
// Update rotation at the GLVolumes.
selection.synchronize_unselected_instances(Selection::SYNC_ROTATION_GENERAL);
selection.synchronize_unselected_volumes();
// Copy rotation values from GLVolumes into Model (ModelInstance / ModelVolume), trigger background processing.
canvas->do_rotate(L("Reset Rotation"));
UpdateAndShow(true);
});
editors_grid_sizer->Add(m_reset_rotation_button);
for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++)
add_edit_boxes("scale", axis_idx);
wxStaticText* scale_unit{ nullptr };
add_unit_text("%", &scale_unit);
// Add reset scale button
m_reset_scale_button = new ScalableButton(parent, wxID_ANY, ScalableBitmap(parent, "undo"));
m_reset_scale_button->SetToolTip(_L("Reset scale"));
m_reset_scale_button->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _L("Reset scale"));
change_scale_value(0, 100.);
change_scale_value(1, 100.);
change_scale_value(2, 100.);
});
editors_grid_sizer->Add(m_reset_scale_button);
for (size_t axis_idx = 0; axis_idx < sizeof(axes); axis_idx++)
add_edit_boxes("size", axis_idx);
add_unit_text(m_imperial_units ? L("in") : L("mm"), &m_size_unit);
editors_grid_sizer->AddStretchSpacer(1);
m_main_grid_sizer->Add(editors_grid_sizer, 1, wxEXPAND);
m_check_inch = new wxCheckBox(parent, wxID_ANY, _L("Inches"));
m_check_inch->SetFont(wxGetApp().normal_font());
m_check_inch->SetValue(m_imperial_units);
m_check_inch->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) {
wxGetApp().app_config->set("use_inches", m_check_inch->GetValue() ? "1" : "0");
wxGetApp().sidebar().update_ui_from_settings();
});
m_main_grid_sizer->Add(m_check_inch, 1, wxEXPAND);
m_og->activate();
m_og->sizer->Clear(true);
m_og->sizer->Add(m_main_grid_sizer, 1, wxEXPAND | wxALL, border);
}
void ObjectManipulation::Show(const bool show)
{
if (show != IsShown()) {
// Show all lines of the panel. Some of these lines will be hidden in the lines below.
m_og->Show(show);
if (show && wxGetApp().get_mode() != comSimple) {
// Show the label and the name of the STL in simple mode only.
// Label "Name: "
m_main_grid_sizer->Show(size_t(0), false);
// The actual name of the STL.
m_main_grid_sizer->Show(size_t(1), false);
}
}
if (show) {
// Show the "World Coordinates" / "Local Coordintes" Combo in Advanced / Expert mode only.
#if ENABLE_WORLD_COORDINATE
const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection();
bool show_world_local_combo = wxGetApp().get_mode() != comSimple && (selection.is_single_full_instance() || selection.is_single_volume() || selection.is_single_modifier());
#else
bool show_world_local_combo = wxGetApp().plater()->canvas3D()->get_selection().is_single_full_instance() && wxGetApp().get_mode() != comSimple;
#endif // ENABLE_WORLD_COORDINATE
m_word_local_combo->Show(show_world_local_combo);
m_empty_str->Show(!show_world_local_combo);
}
}
bool ObjectManipulation::IsShown()
{
return dynamic_cast<const wxStaticBoxSizer*>(m_og->sizer)->GetStaticBox()->IsShown(); // m_og->get_grid_sizer()->IsShown(2);
}
void ObjectManipulation::UpdateAndShow(const bool show)
{
if (show) {
this->set_dirty();
this->update_if_dirty();
}
OG_Settings::UpdateAndShow(show);
}
void ObjectManipulation::update_ui_from_settings()
{
if (m_imperial_units != (wxGetApp().app_config->get("use_inches") == "1")) {
m_imperial_units = wxGetApp().app_config->get("use_inches") == "1";
auto update_unit_text = [](const wxString& new_unit_text, wxStaticText* widget) {
widget->SetLabel(new_unit_text);
if (wxOSX) set_font_and_background_style(widget, wxGetApp().normal_font());
};
update_unit_text(m_imperial_units ? _L("in") : _L("mm"), m_position_unit);
update_unit_text(m_imperial_units ? _L("in") : _L("mm"), m_size_unit);
for (int i = 0; i < 3; ++i) {
auto update = [this, i](/*ManipulationEditorKey*/int key_id, const Vec3d& new_value) {
wxString new_text = double_to_string(m_imperial_units ? new_value(i) * mm_to_in : new_value(i), 2);
const int id = key_id * 3 + i;
if (id >= 0) m_editors[id]->set_value(new_text);
};
update(0/*mePosition*/, m_new_position);
update(3/*meSize*/, m_new_size);
}
}
m_check_inch->SetValue(m_imperial_units);
if (m_use_colors != (wxGetApp().app_config->get("color_mapinulation_panel") == "1"))
{
m_use_colors = wxGetApp().app_config->get("color_mapinulation_panel") == "1";
// update colors for edit-boxes
int axis_id = 0;
for (ManipulationEditor* editor : m_editors) {
// editor->SetForegroundColour(m_use_colors ? wxColour(axes_color_text[axis_id]) : wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT));
if (m_use_colors) {
editor->SetBackgroundColour(wxColour(axes_color_back[axis_id]));
if (wxGetApp().dark_mode())
editor->SetForegroundColour(*wxBLACK);
}
else {
#ifdef _WIN32
wxGetApp().UpdateDarkUI(editor);
#else
editor->SetBackgroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW));
editor->SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT));
#endif /* _WIN32 */
}
editor->Refresh();
if (++axis_id == 3)
axis_id = 0;
}
}
}
void ObjectManipulation::update_settings_value(const Selection& selection)
{
m_new_move_label_string = L("Position");
m_new_rotate_label_string = L("Rotation");
m_new_scale_label_string = L("Scale factors");
if (wxGetApp().get_mode() == comSimple)
m_world_coordinates = true;
ObjectList* obj_list = wxGetApp().obj_list();
if (selection.is_single_full_instance()) {
// all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
#if !ENABLE_WORLD_COORDINATE
m_new_position = volume->get_instance_offset();
#endif // !ENABLE_WORLD_COORDINATE
// Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible.
if (m_world_coordinates && ! m_uniform_scale &&
! Geometry::is_rotation_ninety_degrees(volume->get_instance_rotation())) {
// Manipulating an instance in the world coordinate system, rotation is not multiples of ninety degrees, therefore enforce uniform scaling.
m_uniform_scale = true;
m_lock_bnt->SetLock(true);
}
if (m_world_coordinates) {
#if ENABLE_WORLD_COORDINATE
m_new_position = volume->get_instance_offset();
#endif // ENABLE_WORLD_COORDINATE
m_new_rotate_label_string = L("Rotate");
m_new_rotation = Vec3d::Zero();
m_new_size = selection.get_scaled_instance_bounding_box().size();
m_new_scale = m_new_size.cwiseProduct(selection.get_unscaled_instance_bounding_box().size().cwiseInverse()) * 100.0;
}
else {
#if ENABLE_WORLD_COORDINATE
m_new_position = Vec3d::Zero();
#endif // ENABLE_WORLD_COORDINATE
m_new_rotation = volume->get_instance_rotation() * (180.0 / M_PI);
m_new_size = volume->get_instance_scaling_factor().cwiseProduct(wxGetApp().model().objects[volume->object_idx()]->raw_mesh_bounding_box().size());
m_new_scale = volume->get_instance_scaling_factor() * 100.;
}
m_new_enabled = true;
}
else if (selection.is_single_full_object() && obj_list->is_selected(itObject)) {
const BoundingBoxf3& box = selection.get_bounding_box();
m_new_position = box.center();
m_new_rotation = Vec3d::Zero();
m_new_scale = Vec3d(100., 100., 100.);
m_new_size = box.size();
m_new_rotate_label_string = L("Rotate");
m_new_scale_label_string = L("Scale");
m_new_enabled = true;
}
else if (selection.is_single_modifier() || selection.is_single_volume()) {
// the selection contains a single volume
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
#if ENABLE_WORLD_COORDINATE
if (m_world_coordinates) {
const Geometry::Transformation trafo(volume->world_matrix());
const Vec3d& offset = trafo.get_offset();
const Vec3d& rotation = trafo.get_rotation();
const Vec3d& scaling_factor = trafo.get_scaling_factor();
const Vec3d& m = trafo.get_mirror();
m_new_position = offset;
m_new_rotation = rotation * (180.0 / M_PI);
m_new_scale = scaling_factor * 100.0;
m_new_size = volume->bounding_box().size().cwiseProduct(scaling_factor);
}
else {
#endif // ENABLE_WORLD_COORDINATE
m_new_position = volume->get_volume_offset();
m_new_rotation = volume->get_volume_rotation() * (180. / M_PI);
m_new_scale = volume->get_volume_scaling_factor() * 100.;
m_new_size = volume->get_instance_scaling_factor().cwiseProduct(volume->get_volume_scaling_factor().cwiseProduct(volume->bounding_box().size()));
#if ENABLE_WORLD_COORDINATE
}
#endif // ENABLE_WORLD_COORDINATE
m_new_enabled = true;
}
else if (obj_list->multiple_selection() || obj_list->is_selected(itInstanceRoot)) {
reset_settings_value();
m_new_move_label_string = L("Translate");
m_new_rotate_label_string = L("Rotate");
m_new_scale_label_string = L("Scale");
m_new_size = selection.get_bounding_box().size();
m_new_enabled = true;
}
else {
// No selection, reset the cache.
// assert(selection.is_empty());
reset_settings_value();
}
}
void ObjectManipulation::update_if_dirty()
{
if (! m_dirty)
return;
const Selection &selection = wxGetApp().plater()->canvas3D()->get_selection();
this->update_settings_value(selection);
auto update_label = [](wxString &label_cache, const std::string &new_label, wxStaticText *widget) {
wxString new_label_localized = _(new_label) + ":";
if (label_cache != new_label_localized) {
label_cache = new_label_localized;
widget->SetLabel(new_label_localized);
if (wxOSX) set_font_and_background_style(widget, wxGetApp().normal_font());
}
};
update_label(m_cache.move_label_string, m_new_move_label_string, m_move_Label);
update_label(m_cache.rotate_label_string, m_new_rotate_label_string, m_rotate_Label);
update_label(m_cache.scale_label_string, m_new_scale_label_string, m_scale_Label);
enum ManipulationEditorKey
{
mePosition = 0,
meRotation,
meScale,
meSize
};
for (int i = 0; i < 3; ++ i) {
auto update = [this, i](Vec3d &cached, Vec3d &cached_rounded, ManipulationEditorKey key_id, const Vec3d &new_value) {
wxString new_text = double_to_string(new_value(i), 2);
double new_rounded;
new_text.ToDouble(&new_rounded);
if (std::abs(cached_rounded(i) - new_rounded) > EPSILON) {
cached_rounded(i) = new_rounded;
const int id = key_id*3+i;
if (m_imperial_units && (key_id == mePosition || key_id == meSize))
new_text = double_to_string(new_value(i)*mm_to_in, 2);
if (id >= 0) m_editors[id]->set_value(new_text);
}
cached(i) = new_value(i);
};
update(m_cache.position, m_cache.position_rounded, mePosition, m_new_position);
update(m_cache.scale, m_cache.scale_rounded, meScale, m_new_scale);
update(m_cache.size, m_cache.size_rounded, meSize, m_new_size);
update(m_cache.rotation, m_cache.rotation_rounded, meRotation, m_new_rotation);
}
if (selection.requires_uniform_scale()) {
m_lock_bnt->SetLock(true);
m_lock_bnt->SetToolTip(_L("You cannot use non-uniform scaling mode for multiple objects/parts selection"));
m_lock_bnt->disable();
}
else {
m_lock_bnt->SetLock(m_uniform_scale);
m_lock_bnt->SetToolTip(wxEmptyString);
m_lock_bnt->enable();
}
{
int new_selection = m_world_coordinates ? 0 : 1;
if (m_word_local_combo->GetSelection() != new_selection)
m_word_local_combo->SetSelection(new_selection);
}
if (m_new_enabled)
m_og->enable();
else
m_og->disable();
update_reset_buttons_visibility();
update_mirror_buttons_visibility();
m_dirty = false;
}
void ObjectManipulation::update_reset_buttons_visibility()
{
GLCanvas3D* canvas = wxGetApp().plater()->canvas3D();
if (!canvas)
return;
const Selection& selection = canvas->get_selection();
bool show_rotation = false;
bool show_scale = false;
bool show_drop_to_bed = false;
if (selection.is_single_full_instance() || selection.is_single_modifier() || selection.is_single_volume()) {
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
Vec3d rotation;
Vec3d scale;
double min_z = 0.;
if (selection.is_single_full_instance()) {
rotation = volume->get_instance_rotation();
scale = volume->get_instance_scaling_factor();
min_z = wxGetApp().model().objects[volume->composite_id.object_id]->bounding_box().min.z();
}
else {
rotation = volume->get_volume_rotation();
scale = volume->get_volume_scaling_factor();
min_z = get_volume_min_z(*volume);
}
show_rotation = !rotation.isApprox(Vec3d::Zero());
show_scale = !scale.isApprox(Vec3d::Ones());
show_drop_to_bed = std::abs(min_z) > SINKING_Z_THRESHOLD;
}
wxGetApp().CallAfter([this, show_rotation, show_scale, show_drop_to_bed] {
// There is a case (under OSX), when this function is called after the Manipulation panel is hidden
// So, let check if Manipulation panel is still shown for this moment
if (!this->IsShown())
return;
m_reset_rotation_button->Show(show_rotation);
m_reset_scale_button->Show(show_scale);
m_drop_to_bed_button->Show(show_drop_to_bed);
// Because of CallAfter we need to layout sidebar after Show/hide of reset buttons one more time
Sidebar& panel = wxGetApp().sidebar();
if (!panel.IsFrozen()) {
panel.Freeze();
panel.Layout();
panel.Thaw();
}
});
}
void ObjectManipulation::update_mirror_buttons_visibility()
{
GLCanvas3D* canvas = wxGetApp().plater()->canvas3D();
Selection& selection = canvas->get_selection();
std::array<MirrorButtonState, 3> new_states = {mbHidden, mbHidden, mbHidden};
if (!m_world_coordinates) {
if (selection.is_single_full_instance() || selection.is_single_modifier() || selection.is_single_volume()) {
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
Vec3d mirror;
if (selection.is_single_full_instance())
mirror = volume->get_instance_mirror();
else
mirror = volume->get_volume_mirror();
for (unsigned char i=0; i<3; ++i)
new_states[i] = (mirror[i] < 0. ? mbActive : mbShown);
}
}
else {
// the mirroring buttons should be hidden in world coordinates,
// unless we make it actually mirror in world coords.
}
// Hiding the buttons through Hide() always messed up the sizers. As a workaround, the button
// is assigned a transparent bitmap. We must of course remember the actual state.
wxGetApp().CallAfter([this, new_states]{
for (int i=0; i<3; ++i) {
if (new_states[i] != m_mirror_buttons[i].second) {
const ScalableBitmap* bmp = nullptr;
switch (new_states[i]) {
case mbHidden : bmp = &m_mirror_bitmap_hidden; m_mirror_buttons[i].first->Enable(false); break;
case mbShown : bmp = &m_mirror_bitmap_off; m_mirror_buttons[i].first->Enable(true); break;
case mbActive : bmp = &m_mirror_bitmap_on; m_mirror_buttons[i].first->Enable(true); break;
}
m_mirror_buttons[i].first->SetBitmap_(*bmp);
m_mirror_buttons[i].second = new_states[i];
}
}
});
}
#ifndef __APPLE__
void ObjectManipulation::emulate_kill_focus()
{
if (!m_focused_editor)
return;
m_focused_editor->kill_focus(this);
}
#endif // __APPLE__
void ObjectManipulation::update_item_name(const wxString& item_name)
{
m_item_name->SetLabel(item_name);
}
void ObjectManipulation::update_warning_icon_state(const MeshErrorsInfo& warning)
{
if (const std::string& warning_icon_name = warning.warning_icon_name;
!warning_icon_name.empty())
m_manifold_warning_bmp = ScalableBitmap(m_parent, warning_icon_name);
const wxString& tooltip = warning.tooltip;
m_fix_throught_netfab_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp());
m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0,0) : m_manifold_warning_bmp.bmp().GetSize());
m_fix_throught_netfab_bitmap->SetToolTip(tooltip);
}
void ObjectManipulation::reset_settings_value()
{
m_new_position = Vec3d::Zero();
m_new_rotation = Vec3d::Zero();
m_new_scale = Vec3d::Ones() * 100.;
m_new_size = Vec3d::Zero();
m_new_enabled = false;
// no need to set the dirty flag here as this method is called from update_settings_value(),
// which is called from update_if_dirty(), which resets the dirty flag anyways.
// m_dirty = true;
}
void ObjectManipulation::change_position_value(int axis, double value)
{
if (std::abs(m_cache.position_rounded(axis) - value) < EPSILON)
return;
Vec3d position = m_cache.position;
position(axis) = value;
auto canvas = wxGetApp().plater()->canvas3D();
Selection& selection = canvas->get_selection();
selection.start_dragging();
#if ENABLE_WORLD_COORDINATE
selection.translate(position - m_cache.position, !m_world_coordinates);
#else
selection.translate(position - m_cache.position, selection.requires_local_axes());
#endif // ENABLE_WORLD_COORDINATE
canvas->do_move(L("Set Position"));
m_cache.position = position;
m_cache.position_rounded(axis) = DBL_MAX;
this->UpdateAndShow(true);
}
void ObjectManipulation::change_rotation_value(int axis, double value)
{
if (std::abs(m_cache.rotation_rounded(axis) - value) < EPSILON)
return;
Vec3d rotation = m_cache.rotation;
rotation(axis) = value;
GLCanvas3D* canvas = wxGetApp().plater()->canvas3D();
Selection& selection = canvas->get_selection();
TransformationType transformation_type(TransformationType::World_Relative_Joint);
if (selection.is_single_full_instance() || selection.requires_local_axes())
transformation_type.set_independent();
if (selection.is_single_full_instance() && ! m_world_coordinates) {
//FIXME Selection::rotate() does not process absoulte rotations correctly: It does not recognize the axis index, which was changed.
// transformation_type.set_absolute();
transformation_type.set_local();
}
selection.start_dragging();
selection.rotate(
(M_PI / 180.0) * (transformation_type.absolute() ? rotation : rotation - m_cache.rotation),
transformation_type);
canvas->do_rotate(L("Set Orientation"));
m_cache.rotation = rotation;
m_cache.rotation_rounded(axis) = DBL_MAX;
this->UpdateAndShow(true);
}
void ObjectManipulation::change_scale_value(int axis, double value)
{
if (std::abs(m_cache.scale_rounded(axis) - value) < EPSILON)
return;
Vec3d scale = m_cache.scale;
scale(axis) = value;
this->do_scale(axis, 0.01 * scale);
m_cache.scale = scale;
m_cache.scale_rounded(axis) = DBL_MAX;
this->UpdateAndShow(true);
}
void ObjectManipulation::change_size_value(int axis, double value)
{
if (std::abs(m_cache.size_rounded(axis) - value) < EPSILON)
return;
Vec3d size = m_cache.size;
size(axis) = value;
const Selection& selection = wxGetApp().plater()->canvas3D()->get_selection();
Vec3d ref_size = m_cache.size;
if (selection.is_single_volume() || selection.is_single_modifier()) {
const GLVolume* v = selection.get_volume(*selection.get_volume_idxs().begin());
const Vec3d local_size = size.cwiseQuotient(v->get_instance_scaling_factor());
const Vec3d local_ref_size = v->bounding_box().size().cwiseProduct(v->get_volume_scaling_factor());
const Vec3d local_change = local_size.cwiseQuotient(local_ref_size);
size = local_change.cwiseProduct(v->get_volume_scaling_factor());
ref_size = Vec3d::Ones();
}
else if (selection.is_single_full_instance())
ref_size = m_world_coordinates ?
selection.get_unscaled_instance_bounding_box().size() :
wxGetApp().model().objects[selection.get_volume(*selection.get_volume_idxs().begin())->object_idx()]->raw_mesh_bounding_box().size();
this->do_scale(axis, size.cwiseQuotient(ref_size));
m_cache.size = size;
m_cache.size_rounded(axis) = DBL_MAX;
this->UpdateAndShow(true);
}
void ObjectManipulation::do_scale(int axis, const Vec3d &scale) const
{
Selection& selection = wxGetApp().plater()->canvas3D()->get_selection();
Vec3d scaling_factor = scale;
TransformationType transformation_type(TransformationType::World_Relative_Joint);
if (selection.is_single_full_instance()) {
transformation_type.set_absolute();
if (! m_world_coordinates)
transformation_type.set_local();
}
if (m_uniform_scale || selection.requires_uniform_scale())
scaling_factor = scale(axis) * Vec3d::Ones();
selection.start_dragging();
selection.scale(scaling_factor, transformation_type);
wxGetApp().plater()->canvas3D()->do_scale(L("Set Scale"));
}
void ObjectManipulation::on_change(const std::string& opt_key, int axis, double new_value)
{
if (!m_cache.is_valid())
return;
if (m_imperial_units && (opt_key == "position" || opt_key == "size"))
new_value *= in_to_mm;
if (opt_key == "position")
change_position_value(axis, new_value);
else if (opt_key == "rotation")
change_rotation_value(axis, new_value);
else if (opt_key == "scale")
change_scale_value(axis, new_value);
else if (opt_key == "size")
change_size_value(axis, new_value);
}
void ObjectManipulation::set_uniform_scaling(const bool new_value)
{
const Selection &selection = wxGetApp().plater()->canvas3D()->get_selection();
if (selection.is_single_full_instance() && m_world_coordinates && !new_value) {
// Verify whether the instance rotation is multiples of 90 degrees, so that the scaling in world coordinates is possible.
// all volumes in the selection belongs to the same instance, any of them contains the needed instance data, so we take the first one
const GLVolume* volume = selection.get_volume(*selection.get_volume_idxs().begin());
// Is the angle close to a multiple of 90 degrees?
if (! Geometry::is_rotation_ninety_degrees(volume->get_instance_rotation())) {
// Cannot apply scaling in the world coordinate system.
//wxMessageDialog dlg(GUI::wxGetApp().mainframe,
MessageDialog dlg(GUI::wxGetApp().mainframe,
_L("The currently manipulated object is tilted (rotation angles are not multiples of 90°).\n"
"Non-uniform scaling of tilted objects is only possible in the World coordinate system,\n"
"once the rotation is embedded into the object coordinates.") + "\n" +
_L("This operation is irreversible.\n"
"Do you want to proceed?"),
SLIC3R_APP_NAME,
wxYES_NO | wxCANCEL | wxCANCEL_DEFAULT | wxICON_QUESTION);
if (dlg.ShowModal() != wxID_YES) {
// Enforce uniform scaling.
m_lock_bnt->SetLock(true);
return;
}
// Bake the rotation into the meshes of the object.
wxGetApp().model().objects[volume->composite_id.object_id]->bake_xy_rotation_into_meshes(volume->composite_id.instance_id);
// Update the 3D scene, selections etc.
wxGetApp().plater()->update();
// Recalculate cached values at this panel, refresh the screen.
this->UpdateAndShow(true);
}
}
m_uniform_scale = new_value;
}
#if ENABLE_WORLD_COORDINATE
void ObjectManipulation::set_world_coordinates(const bool world_coordinates)
{
m_world_coordinates = world_coordinates;
this->UpdateAndShow(true);
GLCanvas3D* canvas = wxGetApp().plater()->canvas3D();
canvas->set_as_dirty();
canvas->request_extra_frame();
}
#endif // ENABLE_WORLD_COORDINATE
void ObjectManipulation::msw_rescale()
{
const int em = wxGetApp().em_unit();
m_item_name->SetMinSize(wxSize(20*em, wxDefaultCoord));
msw_rescale_word_local_combo(m_word_local_combo);
m_word_local_combo_sizer->SetMinSize(wxSize(-1, m_word_local_combo->GetBestHeight(-1)));
m_manifold_warning_bmp.msw_rescale();
const wxString& tooltip = m_fix_throught_netfab_bitmap->GetToolTipText();
m_fix_throught_netfab_bitmap->SetBitmap(tooltip.IsEmpty() ? wxNullBitmap : m_manifold_warning_bmp.bmp());
m_fix_throught_netfab_bitmap->SetMinSize(tooltip.IsEmpty() ? wxSize(0, 0) : m_manifold_warning_bmp.bmp().GetSize());
m_mirror_bitmap_on.msw_rescale();
m_mirror_bitmap_off.msw_rescale();
m_mirror_bitmap_hidden.msw_rescale();
m_reset_scale_button->msw_rescale();
m_reset_rotation_button->msw_rescale();
m_drop_to_bed_button->msw_rescale();
m_lock_bnt->msw_rescale();
for (int id = 0; id < 3; ++id)
m_mirror_buttons[id].first->msw_rescale();
// rescale label-heights
// Text trick to grid sizer layout:
// Height of labels should be equivalent to the edit boxes
const int height = wxTextCtrl(parent(), wxID_ANY, "Br").GetBestHeight(-1);
for (wxBoxSizer* sizer : m_rescalable_sizers)
sizer->SetMinSize(wxSize(-1, height));
// rescale edit-boxes
for (ManipulationEditor* editor : m_editors)
editor->msw_rescale();
// rescale "inches" checkbox
m_check_inch->SetMinSize(wxSize(-1, int(1.5f * m_check_inch->GetFont().GetPixelSize().y + 0.5f)));
get_og()->msw_rescale();
}
void ObjectManipulation::sys_color_changed()
{
#ifdef _WIN32
get_og()->sys_color_changed();
wxGetApp().UpdateDarkUI(m_word_local_combo);
wxGetApp().UpdateDarkUI(m_check_inch);
#endif
for (ManipulationEditor* editor : m_editors)
editor->sys_color_changed(this);
// btn...->msw_rescale() updates icon on button, so use it
m_mirror_bitmap_on.msw_rescale();
m_mirror_bitmap_off.msw_rescale();
m_mirror_bitmap_hidden.msw_rescale();
m_reset_scale_button->msw_rescale();
m_reset_rotation_button->msw_rescale();
m_drop_to_bed_button->msw_rescale();
m_lock_bnt->msw_rescale();
for (int id = 0; id < 3; ++id)
m_mirror_buttons[id].first->msw_rescale();
}
static const char axes[] = { 'x', 'y', 'z' };
ManipulationEditor::ManipulationEditor(ObjectManipulation* parent,
const std::string& opt_key,
int axis) :
wxTextCtrl(parent->parent(), wxID_ANY, wxEmptyString, wxDefaultPosition,
wxSize((wxOSX ? 5 : 6)*int(wxGetApp().em_unit()), wxDefaultCoord), wxTE_PROCESS_ENTER
#ifdef _WIN32
| wxBORDER_SIMPLE
#endif
),
m_opt_key(opt_key),
m_axis(axis)
{
set_font_and_background_style(this, wxGetApp().normal_font());
#ifdef __WXOSX__
this->OSXDisableAllSmartSubstitutions();
#endif // __WXOSX__
if (parent->use_colors()) {
this->SetBackgroundColour(wxColour(axes_color_back[axis]));
this->SetForegroundColour(*wxBLACK);
} else {
wxGetApp().UpdateDarkUI(this);
}
// A name used to call handle_sidebar_focus_event()
m_full_opt_name = m_opt_key+"_"+axes[axis];
// Reset m_enter_pressed flag to _false_, when value is editing
this->Bind(wxEVT_TEXT, [this](wxEvent&) { m_enter_pressed = false; }, this->GetId());
this->Bind(wxEVT_TEXT_ENTER, [this, parent](wxEvent&)
{
m_enter_pressed = true;
parent->on_change(m_opt_key, m_axis, get_value());
}, this->GetId());
this->Bind(wxEVT_KILL_FOCUS, [this, parent](wxFocusEvent& e)
{
parent->set_focused_editor(nullptr);
if (!m_enter_pressed)
kill_focus(parent);
e.Skip();
}, this->GetId());
this->Bind(wxEVT_SET_FOCUS, [this, parent](wxFocusEvent& e)
{
parent->set_focused_editor(this);
// needed to show the visual hints in 3D scene
wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(m_full_opt_name, true);
e.Skip();
}, this->GetId());
this->Bind(wxEVT_CHAR, ([this](wxKeyEvent& event)
{
// select all text using Ctrl+A
if (wxGetKeyState(wxKeyCode('A')) && wxGetKeyState(WXK_CONTROL))
this->SetSelection(-1, -1); //select all
event.Skip();
}));
}
void ManipulationEditor::msw_rescale()
{
const int em = wxGetApp().em_unit();
SetMinSize(wxSize(5 * em, wxDefaultCoord));
}
void ManipulationEditor::sys_color_changed(ObjectManipulation* parent)
{
if (parent->use_colors())
SetForegroundColour(*wxBLACK);
else
#ifdef _WIN32
wxGetApp().UpdateDarkUI(this);
#else
SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT));
#endif // _WIN32
}
double ManipulationEditor::get_value()
{
wxString str = GetValue();
double value;
const char dec_sep = is_decimal_separator_point() ? '.' : ',';
const char dec_sep_alt = dec_sep == '.' ? ',' : '.';
// Replace the first incorrect separator in decimal number.
if (str.Replace(dec_sep_alt, dec_sep, false) != 0)
SetValue(str);
if (str == ".")
value = 0.0;
if ((str.IsEmpty() || !str.ToDouble(&value)) && !m_valid_value.IsEmpty()) {
str = m_valid_value;
SetValue(str);
str.ToDouble(&value);
}
return value;
}
void ManipulationEditor::set_value(const wxString& new_value)
{
if (new_value.IsEmpty())
return;
m_valid_value = new_value;
SetValue(m_valid_value);
}
void ManipulationEditor::kill_focus(ObjectManipulation* parent)
{
parent->on_change(m_opt_key, m_axis, get_value());
// if the change does not come from the user pressing the ENTER key
// we need to hide the visual hints in 3D scene
wxGetApp().plater()->canvas3D()->handle_sidebar_focus_event(m_full_opt_name, false);
}
} //namespace GUI
} //namespace Slic3r