///|/ Copyright (c) Prusa Research 2019 - 2023 Oleksandra Iushchenko @YuSanka, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Filip Sykala @Jony01, Vojtěch Král @vojtechkral ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher ///|/ #include "GLGizmoCut.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include #include #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "slic3r/GUI/Gizmos/GizmoObjectManipulation.hpp" #include "slic3r/GUI/format.hpp" #include "slic3r/Utils/UndoRedo.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/TriangleMeshSlicer.hpp" #include "imgui/imgui_internal.h" #include "slic3r/GUI/Field.hpp" #include "slic3r/GUI/MsgDialog.hpp" namespace Slic3r { namespace GUI { static const ColorRGBA GRABBER_COLOR = ColorRGBA::YELLOW(); static const ColorRGBA UPPER_PART_COLOR = ColorRGBA::CYAN(); static const ColorRGBA LOWER_PART_COLOR = ColorRGBA::MAGENTA(); static const ColorRGBA MODIFIER_COLOR = ColorRGBA(0.75f, 0.75f, 0.75f, 0.5f); // connector colors static const ColorRGBA PLAG_COLOR = ColorRGBA::YELLOW(); static const ColorRGBA DOWEL_COLOR = ColorRGBA::DARK_YELLOW(); static const ColorRGBA HOVERED_PLAG_COLOR = ColorRGBA::CYAN(); static const ColorRGBA HOVERED_DOWEL_COLOR = ColorRGBA(0.0f, 0.5f, 0.5f, 1.0f); static const ColorRGBA SELECTED_PLAG_COLOR = ColorRGBA::GRAY(); static const ColorRGBA SELECTED_DOWEL_COLOR = ColorRGBA::DARK_GRAY(); static const ColorRGBA CONNECTOR_DEF_COLOR = ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f); static const ColorRGBA CONNECTOR_ERR_COLOR = ColorRGBA(1.0f, 0.3f, 0.3f, 0.5f); static const ColorRGBA HOVERED_ERR_COLOR = ColorRGBA(1.0f, 0.3f, 0.3f, 1.0f); static const ColorRGBA CUT_PLANE_DEF_COLOR = ColorRGBA(0.9f, 0.9f, 0.9f, 0.5f); static const ColorRGBA CUT_PLANE_ERR_COLOR = ColorRGBA(1.0f, 0.8f, 0.8f, 0.5f); const unsigned int AngleResolution = 64; const unsigned int ScaleStepsCount = 72; const float ScaleStepRad = 2.0f * float(PI) / ScaleStepsCount; const unsigned int ScaleLongEvery = 2; const float ScaleLongTooth = 0.1f; // in percent of radius const unsigned int SnapRegionsCount = 8; const float UndefFloat = -999.f; const std::string UndefLabel = " "; using namespace Geometry; // Generates mesh for a line static GLModel::Geometry its_make_line(Vec3f beg_pos, Vec3f end_pos) { GLModel::Geometry init_data; init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; init_data.reserve_vertices(2); init_data.reserve_indices(2); // vertices init_data.add_vertex(beg_pos); init_data.add_vertex(end_pos); // indices init_data.add_line(0, 1); return init_data; } //! -- #ysFIXME those functions bodies are ported from GizmoRotation // Generates mesh for a circle static void init_from_circle(GLModel& model, double radius) { GLModel::Geometry init_data; init_data.format = { GLModel::Geometry::EPrimitiveType::LineLoop, GLModel::Geometry::EVertexLayout::P3 }; init_data.reserve_vertices(ScaleStepsCount); init_data.reserve_indices(ScaleStepsCount); // vertices + indices for (unsigned int i = 0; i < ScaleStepsCount; ++i) { const float angle = float(i * ScaleStepRad); init_data.add_vertex(Vec3f(::cos(angle) * float(radius), ::sin(angle) * float(radius), 0.0f)); init_data.add_index(i); } model.init_from(std::move(init_data)); model.set_color(ColorRGBA::WHITE()); } // Generates mesh for a scale static void init_from_scale(GLModel& model, double radius) { const float out_radius_long = float(radius) * (1.0f + ScaleLongTooth); const float out_radius_short = float(radius) * (1.0f + 0.5f * ScaleLongTooth); GLModel::Geometry init_data; init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; init_data.reserve_vertices(2 * ScaleStepsCount); init_data.reserve_indices(2 * ScaleStepsCount); // vertices + indices for (unsigned int i = 0; i < ScaleStepsCount; ++i) { const float angle = float(i * ScaleStepRad); const float cosa = ::cos(angle); const float sina = ::sin(angle); const float in_x = cosa * float(radius); const float in_y = sina * float(radius); const float out_x = (i % ScaleLongEvery == 0) ? cosa * out_radius_long : cosa * out_radius_short; const float out_y = (i % ScaleLongEvery == 0) ? sina * out_radius_long : sina * out_radius_short; // vertices init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); // indices init_data.add_line(i * 2, i * 2 + 1); } model.init_from(std::move(init_data)); model.set_color(ColorRGBA::WHITE()); } // Generates mesh for a snap_radii static void init_from_snap_radii(GLModel& model, double radius) { const float step = 2.0f * float(PI) / float(SnapRegionsCount); const float in_radius = float(radius) / 3.0f; const float out_radius = 2.0f * in_radius; GLModel::Geometry init_data; init_data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 }; init_data.reserve_vertices(2 * ScaleStepsCount); init_data.reserve_indices(2 * ScaleStepsCount); // vertices + indices for (unsigned int i = 0; i < ScaleStepsCount; ++i) { const float angle = float(i) * step; const float cosa = ::cos(angle); const float sina = ::sin(angle); const float in_x = cosa * in_radius; const float in_y = sina * in_radius; const float out_x = cosa * out_radius; const float out_y = sina * out_radius; // vertices init_data.add_vertex(Vec3f(in_x, in_y, 0.0f)); init_data.add_vertex(Vec3f(out_x, out_y, 0.0f)); // indices init_data.add_line(i * 2, i * 2 + 1); } model.init_from(std::move(init_data)); model.set_color(ColorRGBA::WHITE()); } // Generates mesh for a angle_arc static void init_from_angle_arc(GLModel& model, double angle, double radius) { model.reset(); const float step_angle = float(angle) / float(AngleResolution); const float ex_radius = float(radius); GLModel::Geometry init_data; init_data.format = { GLModel::Geometry::EPrimitiveType::LineStrip, GLModel::Geometry::EVertexLayout::P3 }; init_data.reserve_vertices(1 + AngleResolution); init_data.reserve_indices(1 + AngleResolution); // vertices + indices for (unsigned int i = 0; i <= AngleResolution; ++i) { const float angle = float(i) * step_angle; init_data.add_vertex(Vec3f(::cos(angle) * ex_radius, ::sin(angle) * ex_radius, 0.0f)); init_data.add_index(i); } model.init_from(std::move(init_data)); } //! -- GLGizmoCut3D::GLGizmoCut3D(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id) : GLGizmoBase(parent, icon_filename, sprite_id) , m_connectors_group_id (GrabberID::Count) , m_connector_type (CutConnectorType::Plug) , m_connector_style (int(CutConnectorStyle::Prism)) , m_connector_shape_id (int(CutConnectorShape::Circle)) { m_modes = { _u8L("Planar"), _u8L("Dovetail")//, _u8L("Grid") // , _u8L("Radial"), _u8L("Modular") }; m_connector_modes = { _u8L("Auto"), _u8L("Manual") }; std::map connetor_types = { {ImGui::PlugMarker , _u8L("Plug") }, {ImGui::DowelMarker, _u8L("Dowel") }, //TRN Connectors type next to "Plug" and "Dowel" {ImGui::SnapMarker, _u8L("Snap") }, }; for (auto connector : connetor_types) { std::string type_label = " " + connector.second + " "; type_label += connector.first; m_connector_types.push_back(type_label); } m_connector_styles = { _u8L("Prism"), _u8L("Frustum") // , _u8L("Claw") }; m_connector_shapes = { _u8L("Triangle"), _u8L("Square"), _u8L("Hexagon"), _u8L("Circle") // , _u8L("D-shape") }; m_axis_names = { "X", "Y", "Z" }; m_part_orientation_names = { {"none", _L("Keep orientation")}, {"on_cut", _L("Place on cut")}, {"flip", _L("Flip upside down")}, }; m_labels_map = { {"Connectors" , _u8L("Connectors")}, {"Type" , _u8L("Type")}, {"Style" , _u8L("Style")}, {"Shape" , _u8L("Shape")}, {"Depth" , _u8L("Depth")}, {"Size" , _u8L("Size")}, {"Rotation" , _u8L("Rotation")}, {"Groove" , _u8L("Groove")}, {"Width" , _u8L("Width")}, {"Flap Angle" , _u8L("Flap Angle")}, {"Groove Angle" , _u8L("Groove Angle")}, }; // update_connector_shape(); } std::string GLGizmoCut3D::get_tooltip() const { std::string tooltip; if (m_hover_id == Z || (m_dragging && m_hover_id == CutPlane)) { double koef = m_imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0; std::string unit_str = " " + (m_imperial_units ? _u8L("in") : _u8L("mm")); const BoundingBoxf3& tbb = m_transformed_bounding_box; const std::string name = m_keep_as_parts ? _u8L("Part") : _u8L("Object"); if (tbb.max.z() >= 0.0) { double top = (tbb.min.z() <= 0.0 ? tbb.max.z() : tbb.size().z()) * koef; tooltip += format(static_cast(top), 2) + " " + unit_str + " (" + name + " A)"; if (tbb.min.z() <= 0.0) tooltip += "\n"; } if (tbb.min.z() <= 0.0) { double bottom = (tbb.max.z() <= 0.0 ? tbb.size().z() : (tbb.min.z() * (-1))) * koef; tooltip += format(static_cast(bottom), 2) + " " + unit_str + " (" + name + " B)"; } return tooltip; } if (!m_dragging && m_hover_id == CutPlane) { if (CutMode(m_mode) == CutMode::cutTongueAndGroove) return _u8L("Click to flip the cut plane\n" "Drag to move the cut plane"); return _u8L("Click to flip the cut plane\n" "Drag to move the cut plane\n" "Right-click a part to assign it to the other side"); } if (tooltip.empty() && (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation)) { std::string axis = m_hover_id == X ? "X" : m_hover_id == Y ? "Y" : "Z"; return axis + ": " + format(float(rad2deg(m_angle)), 1) + _u8L("°"); } return tooltip; } bool GLGizmoCut3D::on_mouse(const wxMouseEvent &mouse_event) { Vec2i mouse_coord(mouse_event.GetX(), mouse_event.GetY()); Vec2d mouse_pos = mouse_coord.cast(); if (mouse_event.ShiftDown() && mouse_event.LeftDown()) return gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); if (mouse_event.CmdDown() && mouse_event.LeftDown()) return false; if (cut_line_processing()) { if (mouse_event.ShiftDown()) { if (mouse_event.Moving()|| mouse_event.Dragging()) return gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); if (mouse_event.LeftUp()) return gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); } discard_cut_line_processing(); } else if (mouse_event.Moving()) return false; if (m_hover_id >= CutPlane && mouse_event.LeftDown() && !m_connectors_editing) { // before processing of a use_grabbers(), detect start move position as a projection of mouse position to the cut plane Vec3d pos; Vec3d pos_world; if (unproject_on_cut_plane(mouse_pos, pos, pos_world, false)) m_cut_plane_start_move_pos = pos_world; } if (use_grabbers(mouse_event)) { if (m_hover_id >= m_connectors_group_id) { if (mouse_event.LeftDown() && !mouse_event.CmdDown() && !mouse_event.AltDown()) unselect_all_connectors(); if (mouse_event.LeftUp() && !mouse_event.ShiftDown()) gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); } else if (m_hover_id == CutPlane) { if (mouse_event.LeftDown()) { m_was_cut_plane_dragged = m_was_contour_selected = false; // disable / enable current contour Vec3d pos; Vec3d pos_world; m_was_contour_selected = unproject_on_cut_plane(mouse_pos.cast(), pos, pos_world); if (m_was_contour_selected) { // Following would inform the clipper about the mouse click, so it can // toggle the respective contour as disabled. //m_c->object_clipper()->pass_mouse_click(pos_world); //process_contours(); return true; } } else if (mouse_event.LeftUp() && !m_was_cut_plane_dragged && !m_was_contour_selected) flip_cut_plane(); } if (m_hover_id >= CutPlane && mouse_event.Dragging() && !m_connectors_editing) { // if we continue to dragging a cut plane, than update a start move position as a projection of mouse position to the cut plane after processing of a use_grabbers() Vec3d pos; Vec3d pos_world; if (unproject_on_cut_plane(mouse_pos, pos, pos_world, false)) m_cut_plane_start_move_pos = pos_world; } toggle_model_objects_visibility(); return true; } static bool pending_right_up = false; if (mouse_event.LeftDown()) { bool grabber_contains_mouse = (get_hover_id() != -1); const bool shift_down = mouse_event.ShiftDown(); if ((!shift_down || grabber_contains_mouse) && gizmo_event(SLAGizmoEventType::LeftDown, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) return true; } else if (mouse_event.Dragging()) { bool control_down = mouse_event.CmdDown(); if (m_parent.get_move_volume_id() != -1) { // don't allow dragging objects with the Sla gizmo on return true; } if (!control_down && gizmo_event(SLAGizmoEventType::Dragging, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false)) { // the gizmo got the event and took some action, no need to do // anything more here m_parent.set_as_dirty(); return true; } if (control_down && (mouse_event.LeftIsDown() || mouse_event.RightIsDown())) { // CTRL has been pressed while already dragging -> stop current action if (mouse_event.LeftIsDown()) gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), true); else if (mouse_event.RightIsDown()) pending_right_up = false; } } else if (mouse_event.LeftUp() && !m_parent.is_mouse_dragging()) { // in case SLA/FDM gizmo is selected, we just pass the LeftUp event // and stop processing - neither object moving or selecting is // suppressed in that case gizmo_event(SLAGizmoEventType::LeftUp, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), mouse_event.CmdDown()); return true; } else if (mouse_event.RightDown()) { if (! m_connectors_editing && mouse_event.GetModifiers() == wxMOD_NONE && CutMode(m_mode) == CutMode::cutPlanar) { // Check the internal part raycasters. if (! m_part_selection.valid()) process_contours(); m_part_selection.toggle_selection(mouse_pos); check_and_update_connectors_state(); // after a contour is deactivated, its connectors are inside the object return true; } if (m_parent.get_selection().get_object_idx() != -1 && gizmo_event(SLAGizmoEventType::RightDown, mouse_pos, false, false, false)) { // we need to set the following right up as processed to avoid showing // the context menu if the user release the mouse over the object pending_right_up = true; // event was taken care of by the SlaSupports gizmo return true; } } else if (pending_right_up && mouse_event.RightUp()) { pending_right_up = false; return true; } return false; } void GLGizmoCut3D::shift_cut(double delta) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); set_center(m_plane_center + m_cut_normal * delta, true); m_ar_plane_center = m_plane_center; } void GLGizmoCut3D::rotate_vec3d_around_plane_center(Vec3d&vec) { vec = Transformation(translation_transform(m_plane_center) * m_rotation_m * translation_transform(-m_plane_center)).get_matrix() * vec; } void GLGizmoCut3D::put_connectors_on_cut_plane(const Vec3d& cp_normal, double cp_offset) { ModelObject* mo = m_c->selection_info()->model_object(); if (CutConnectors& connectors = mo->cut_connectors; !connectors.empty()) { const float sla_shift = m_c->selection_info()->get_sla_shift(); const Vec3d& instance_offset = mo->instances[m_c->selection_info()->get_active_instance()]->get_offset(); for (auto& connector : connectors) { // convert connetor pos to the world coordinates Vec3d pos = connector.pos + instance_offset; pos[Z] += sla_shift; // scalar distance from point to plane along the normal double distance = -cp_normal.dot(pos) + cp_offset; // move connector connector.pos += distance * cp_normal; } } } // returns true if the camera (forward) is pointing in the negative direction of the cut normal bool GLGizmoCut3D::is_looking_forward() const { const Camera& camera = wxGetApp().plater()->get_camera(); const double dot = camera.get_dir_forward().dot(m_cut_normal); return dot < 0.05; } void GLGizmoCut3D::update_clipper() { // update cut_normal Vec3d normal = m_rotation_m * Vec3d::UnitZ(); normal.normalize(); m_cut_normal = normal; // calculate normal and offset for clipping plane Vec3d beg = m_bb_center; beg[Z] -= m_radius; rotate_vec3d_around_plane_center(beg); m_clp_normal = normal; double offset = normal.dot(m_plane_center); double dist = normal.dot(beg); m_parent.set_color_clip_plane(normal, offset); if (!is_looking_forward()) { // recalculate normal and offset for clipping plane, if camera is looking downward to cut plane normal = m_rotation_m * (-1. * Vec3d::UnitZ()); normal.normalize(); beg = m_bb_center; beg[Z] += m_radius; rotate_vec3d_around_plane_center(beg); m_clp_normal = normal; offset = normal.dot(m_plane_center); dist = normal.dot(beg); } m_c->object_clipper()->set_range_and_pos(normal, offset, dist); put_connectors_on_cut_plane(normal, offset); if (m_raycasters.empty()) on_register_raycasters_for_picking(); else update_raycasters_for_picking_transform(); } void GLGizmoCut3D::set_center(const Vec3d& center, bool update_tbb /*=false*/) { set_center_pos(center, update_tbb); check_and_update_connectors_state(); update_clipper(); } void GLGizmoCut3D::switch_to_mode(size_t new_mode) { m_mode = new_mode; update_raycasters_for_picking(); apply_color_clip_plane_colors(); if (auto oc = m_c->object_clipper()) { m_contour_width = CutMode(m_mode) == CutMode::cutTongueAndGroove ? 0.f : 0.4f; oc->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); } update_plane_model(); reset_cut_by_contours(); } bool GLGizmoCut3D::render_cut_mode_combo() { ImGui::AlignTextToFramePadding(); int selection_idx = int(m_mode); const bool is_changed = m_imgui->combo(_u8L("Mode"), m_modes, selection_idx, 0, m_label_width, m_control_width); if (is_changed) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Change cut mode"), UndoRedo::SnapshotType::GizmoAction); switch_to_mode(size_t(selection_idx)); check_and_update_connectors_state(); } return is_changed; } bool GLGizmoCut3D::render_combo(const std::string& label, const std::vector& lines, int& selection_idx) { ImGui::AlignTextToFramePadding(); const bool is_changed = m_imgui->combo(label, lines, selection_idx, 0, m_label_width, m_control_width); //if (is_changed) // update_connector_shape(); return is_changed; } bool GLGizmoCut3D::render_double_input(const std::string& label, double& value_in) { ImGui::AlignTextToFramePadding(); m_imgui->text(label); ImGui::SameLine(m_label_width); ImGui::PushItemWidth(m_control_width); double value = value_in; if (m_imperial_units) value *= GizmoObjectManipulation::mm_to_in; double old_val = value; ImGui::InputDouble(("##" + label).c_str(), &value, 0.0f, 0.0f, "%.2f", ImGuiInputTextFlags_CharsDecimal); ImGui::SameLine(); m_imgui->text(m_imperial_units ? _L("in") : _L("mm")); value_in = value * (m_imperial_units ? GizmoObjectManipulation::in_to_mm : 1.0); return !is_approx(old_val, value); } bool GLGizmoCut3D::render_slider_double_input(const std::string& label, float& value_in, float& tolerance_in, float min_val/* = -0.1f*/, float max_tolerance/* = -0.1f*/) { static constexpr const float UndefMinVal = -0.1f; const float f_mm_to_in = static_cast(GizmoObjectManipulation::mm_to_in); auto render_slider = [this, f_mm_to_in] (const std::string& label, float& val, float def_val, float max_val, const wxString& tooltip) { float min_val = val < 0.f ? UndefMinVal : def_val; float value = val; if (m_imperial_units) { min_val *= f_mm_to_in; value *= f_mm_to_in; } const float old_val = value; const std::string format = val < 0.f ? UndefLabel : (m_imperial_units ? "%.4f " + _u8L("in") : "%.2f " + _u8L("mm")); m_imgui->slider_float(label.c_str(), &value, min_val, max_val, format.c_str(), 1.f, true, tooltip); val = value * (m_imperial_units ? static_cast(GizmoObjectManipulation::in_to_mm) : 1.f); m_is_slider_editing_done |= m_imgui->get_last_slider_status().deactivated_after_edit; return !is_approx(old_val, value); }; const BoundingBoxf3 bbox = m_bounding_box; const float mean_size = float((bbox.size().x() + bbox.size().y() + bbox.size().z()) / 9.0) * (m_imperial_units ? f_mm_to_in : 1.f); const float min_v = min_val > 0.f ? /*std::min(max_val, mean_size)*/min_val : 1.f; m_imgui->text(label); ImGui::SameLine(m_label_width); ImGui::PushItemWidth(m_control_width * 0.7f); // const bool is_value_changed = render_slider("##" + label, value_in, 1.f, mean_size, _L("Value")); const bool is_value_changed = render_slider("##" + label, value_in, min_v, mean_size, _L("Value")); ImGui::SameLine(); ImGui::PushItemWidth(m_control_width * 0.45f); // const bool is_tolerance_changed = render_slider("##tolerance_" + label, tolerance_in, 0.f, 0.5f * mean_size, _L("Tolerance")); const float max_tolerance_v = max_tolerance > 0.f ? std::min(max_tolerance, 0.5f * mean_size) : 0.5f * mean_size; const bool is_tolerance_changed = render_slider("##tolerance_" + label, tolerance_in, 0.f, max_tolerance_v, _L("Tolerance")); return is_value_changed || is_tolerance_changed; } void GLGizmoCut3D::render_move_center_input(int axis) { m_imgui->text(m_axis_names[axis]+":"); ImGui::SameLine(); ImGui::PushItemWidth(0.3f*m_control_width); Vec3d move = m_plane_center; double in_val, value = in_val = move[axis]; if (m_imperial_units) value *= GizmoObjectManipulation::mm_to_in; ImGui::InputDouble(("##move_" + m_axis_names[axis]).c_str(), &value, 0.0, 0.0, "%.2f", ImGuiInputTextFlags_CharsDecimal); ImGui::SameLine(); double val = value * (m_imperial_units ? GizmoObjectManipulation::in_to_mm : 1.0); if (in_val != val) { move[axis] = val; Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); set_center(move, true); m_ar_plane_center = m_plane_center; reset_cut_by_contours(); } } bool GLGizmoCut3D::render_connect_type_radio_button(CutConnectorType type) { ImGui::SameLine(type == CutConnectorType::Plug ? m_label_width : 0); ImGui::PushItemWidth(m_control_width); if (ImGui::RadioButton(m_connector_types[size_t(type)].c_str(), m_connector_type == type)) { m_connector_type = type; // update_connector_shape(); return true; } return false; } void GLGizmoCut3D::render_connect_mode_radio_button(CutConnectorMode mode) { ImGui::SameLine(mode == CutConnectorMode::Auto ? m_label_width : 2 * m_label_width); ImGui::PushItemWidth(m_control_width); if (ImGui::RadioButton(m_connector_modes[int(mode)].c_str(), m_connector_mode == mode)) m_connector_mode = mode; } bool GLGizmoCut3D::render_reset_button(const std::string& label_id, const std::string& tooltip) const { const ImGuiStyle& style = ImGui::GetStyle(); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, { 1, style.ItemSpacing.y }); ImGui::PushStyleColor(ImGuiCol_Button, { 0.25f, 0.25f, 0.25f, 0.0f }); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, { 0.4f, 0.4f, 0.4f, 1.0f }); ImGui::PushStyleColor(ImGuiCol_ButtonActive, { 0.4f, 0.4f, 0.4f, 1.0f }); std::string btn_label; btn_label += ImGui::RevertBtn; const bool revert = ImGui::Button((btn_label +"##" + label_id).c_str()); ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) m_imgui->tooltip(tooltip.c_str(), ImGui::GetFontSize() * 20.0f); ImGui::PopStyleVar(); return revert; } static double get_grabber_mean_size(const BoundingBoxf3& bb) { return (bb.size().x() + bb.size().y() + bb.size().z()) / 30.; } indexed_triangle_set GLGizmoCut3D::its_make_groove_plane() { // values for calculation const float side_width = is_approx(m_groove.flaps_angle, 0.f) ? m_groove.depth : (m_groove.depth / sin(m_groove.flaps_angle)); const float flaps_width = 2.f * side_width * cos(m_groove.flaps_angle); const float groove_half_width_upper = 0.5f * (m_groove.width); const float groove_half_width_lower = 0.5f * (m_groove.width + flaps_width); const float cut_plane_radius = 1.5f * float(m_radius); const float cut_plane_length = 1.5f * cut_plane_radius; const float groove_half_depth = 0.5f * m_groove.depth; const float x = 0.5f * cut_plane_radius; const float y = 0.5f * cut_plane_length; float z_upper = groove_half_depth; float z_lower = -groove_half_depth; const float proj = y * tan(m_groove.angle); float ext_upper_x = groove_half_width_upper + proj; // upper_x extension float ext_lower_x = groove_half_width_lower + proj; // lower_x extension float nar_upper_x = groove_half_width_upper - proj; // upper_x narrowing float nar_lower_x = groove_half_width_lower - proj; // lower_x narrowing const float cut_plane_thiknes = 0.02f;// 0.02f * (float)get_grabber_mean_size(m_bounding_box); // cut_plane_thiknes // Vertices of the groove used to detection if groove is valid // They are written as: // {left_ext_lower, left_nar_lower, left_ext_upper, left_nar_upper, // right_ext_lower, right_nar_lower, right_ext_upper, right_nar_upper } { m_groove_vertices.clear(); m_groove_vertices.reserve(8); m_groove_vertices.emplace_back(Vec3f(-ext_lower_x, -y, z_lower).cast()); m_groove_vertices.emplace_back(Vec3f(-nar_lower_x, y, z_lower).cast()); m_groove_vertices.emplace_back(Vec3f(-ext_upper_x, -y, z_upper).cast()); m_groove_vertices.emplace_back(Vec3f(-nar_upper_x, y, z_upper).cast()); m_groove_vertices.emplace_back(Vec3f( ext_lower_x, -y, z_lower).cast()); m_groove_vertices.emplace_back(Vec3f( nar_lower_x, y, z_lower).cast()); m_groove_vertices.emplace_back(Vec3f( ext_upper_x, -y, z_upper).cast()); m_groove_vertices.emplace_back(Vec3f( nar_upper_x, y, z_upper).cast()); } // Different cases of groove plane: // groove is open if (groove_half_width_upper > proj && groove_half_width_lower > proj) { indexed_triangle_set mesh; auto get_vertices = [x, y](float z_upper, float z_lower, float nar_upper_x, float nar_lower_x, float ext_upper_x, float ext_lower_x) { return std::vector({ // upper left part vertices {-x, -y, z_upper}, {-x, y, z_upper}, {-nar_upper_x, y, z_upper}, {-ext_upper_x, -y, z_upper}, // lower part vertices {-ext_lower_x, -y, z_lower}, {-nar_lower_x, y, z_lower}, {nar_lower_x, y, z_lower}, {ext_lower_x, -y, z_lower}, // upper right part vertices {ext_upper_x, -y, z_upper}, {nar_upper_x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper} }); }; mesh.vertices = get_vertices(z_upper, z_lower, nar_upper_x, nar_lower_x, ext_upper_x, ext_lower_x); mesh.vertices.reserve(2 * mesh.vertices.size()); z_upper -= cut_plane_thiknes; z_lower -= cut_plane_thiknes; const float under_x_shift = cut_plane_thiknes / tan(0.5f * m_groove.flaps_angle); nar_upper_x += under_x_shift; nar_lower_x += under_x_shift; ext_upper_x += under_x_shift; ext_lower_x += under_x_shift; std::vector vertices = get_vertices(z_upper, z_lower, nar_upper_x, nar_lower_x, ext_upper_x, ext_lower_x); mesh.vertices.insert(mesh.vertices.end(), vertices.begin(), vertices.end()); mesh.indices = { // above view {5,4,7}, {5,7,6}, // lower part {3,4,5}, {3,5,2}, // left side {9,6,8}, {8,6,7}, // right side {1,0,2}, {2,0,3}, // upper left part {9,8,10}, {10,8,11}, // upper right part // under view {20,21,22}, {20,22,23}, // upper right part {12,13,14}, {12,14,15}, // upper left part {18,21,20}, {18,20,19}, // right side {16,15,14}, {16,14,17}, // left side {16,17,18}, {16,18,19}, // lower part // left edge {1,13,0}, {0,13,12}, // front edge {0,12,3}, {3,12,15}, {3,15,4}, {4,15,16}, {4,16,7}, {7,16,19}, {7,19,20}, {7,20,8}, {8,20,11}, {11,20,23}, // right edge {11,23,10}, {10,23,22}, // back edge {1,13,2}, {2,13,14}, {2,14,17}, {2,17,5}, {5,17,6}, {6,17,18}, {6,18,9}, {9,18,21}, {9,21,10}, {10,21,22} }; return mesh; } float cross_pt_upper_y = groove_half_width_upper / tan(m_groove.angle); // groove is closed if (groove_half_width_upper < proj && groove_half_width_lower < proj) { float cross_pt_lower_y = groove_half_width_lower / tan(m_groove.angle); indexed_triangle_set mesh; auto get_vertices = [x, y](float z_upper, float z_lower, float cross_pt_upper_y, float cross_pt_lower_y, float ext_upper_x, float ext_lower_x) { return std::vector({ // upper part vertices {-x, -y, z_upper}, {-x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper}, {ext_upper_x, -y, z_upper}, {0.f, cross_pt_upper_y, z_upper}, {-ext_upper_x, -y, z_upper}, // lower part vertices {-ext_lower_x, -y, z_lower}, {0.f, cross_pt_lower_y, z_lower}, {ext_lower_x, -y, z_lower} }); }; mesh.vertices = get_vertices(z_upper, z_lower, cross_pt_upper_y, cross_pt_lower_y, ext_upper_x, ext_lower_x); mesh.vertices.reserve(2 * mesh.vertices.size()); z_upper -= cut_plane_thiknes; z_lower -= cut_plane_thiknes; const float under_x_shift = cut_plane_thiknes / tan(0.5f * m_groove.flaps_angle); cross_pt_upper_y += cut_plane_thiknes; cross_pt_lower_y += cut_plane_thiknes; ext_upper_x += under_x_shift; ext_lower_x += under_x_shift; std::vector vertices = get_vertices(z_upper, z_lower, cross_pt_upper_y, cross_pt_lower_y, ext_upper_x, ext_lower_x); mesh.vertices.insert(mesh.vertices.end(), vertices.begin(), vertices.end()); mesh.indices = { // above view {8,7,9}, // lower part {5,8,6}, {6,8,7}, // left side {4,9,8}, {4,8,5}, // right side {1,0,6}, {1,6,5},{1,5,2}, {2,5,4}, {2,4,3}, // upper part // under view {10,11,16}, {16,11,15}, {15,11,12}, {15,12,14}, {14,12,13}, // upper part {18,15,14}, {14,18,19}, // right side {17,16,15}, {17,15,18}, // left side {17,18,19}, // lower part // left edge {1,11,0}, {0,11,10}, // front edge {0,10,6}, {6,10,16}, {6,17,16}, {6,7,17}, {7,17,19}, {7,19,9}, {4,14,19}, {4,19,9}, {4,14,13}, {4,13,3}, // right edge {3,13,12}, {3,12,2}, // back edge {2,12,11}, {2,11,1} }; return mesh; } // groove is closed from the roof indexed_triangle_set mesh; mesh.vertices = { // upper part vertices {-x, -y, z_upper}, {-x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper}, {ext_upper_x, -y, z_upper}, {0.f, cross_pt_upper_y, z_upper}, {-ext_upper_x, -y, z_upper}, // lower part vertices {-ext_lower_x, -y, z_lower}, {-nar_lower_x, y, z_lower}, {nar_lower_x, y, z_lower}, {ext_lower_x, -y, z_lower} }; mesh.vertices.reserve(2 * mesh.vertices.size() + 1); z_upper -= cut_plane_thiknes; z_lower -= cut_plane_thiknes; const float under_x_shift = cut_plane_thiknes / tan(0.5f * m_groove.flaps_angle); nar_lower_x += under_x_shift; ext_upper_x += under_x_shift; ext_lower_x += under_x_shift; std::vector vertices = { // upper part vertices {-x, -y, z_upper}, {-x, y, z_upper}, {x, y, z_upper}, {x, -y, z_upper}, {ext_upper_x, -y, z_upper}, {under_x_shift, cross_pt_upper_y, z_upper}, {-under_x_shift, cross_pt_upper_y, z_upper}, {-ext_upper_x, -y, z_upper}, // lower part vertices {-ext_lower_x, -y, z_lower}, {-nar_lower_x, y, z_lower}, {nar_lower_x, y, z_lower}, {ext_lower_x, -y, z_lower} }; mesh.vertices.insert(mesh.vertices.end(), vertices.begin(), vertices.end()); mesh.indices = { // above view {8,7,10}, {8,10,9}, // lower part {5,8,7}, {5,7,6}, // left side {4,10,9}, {4,9,5}, // right side {1,0,6}, {1,6,5},{1,5,2}, {2,5,4}, {2,4,3}, // upper part // under view {11,12,18}, {18,12,17}, {17,12,16}, {16,12,13}, {16,13,15}, {15,13,14}, // upper part {21,16,15}, {21,15,22}, // right side {19,18,17}, {19,17,20}, // left side {19,20,21}, {19,21,22}, // lower part // left edge {1,12,11}, {1,11,0}, // front edge {0,11,18}, {0,18,6}, {7,19,18}, {7,18,6}, {7,19,22}, {7,22,10}, {10,22,15}, {10,15,4}, {4,15,14}, {4,14,3}, // right edge {3,14,13}, {3,14,2}, // back edge {2,13,12}, {2,12,1}, {5,16,21}, {5,21,9}, {9,21,20}, {9,20,8}, {5,17,20}, {5,20,8} }; return mesh; } void GLGizmoCut3D::render_cut_plane() { if (cut_line_processing()) return; GLShaderProgram* shader = wxGetApp().get_shader("flat"); if (shader == nullptr) return; glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glDisable(GL_CULL_FACE)); glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); shader->start_using(); const Camera& camera = wxGetApp().plater()->get_camera(); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); ColorRGBA cp_clr = can_perform_cut() && has_valid_groove() ? CUT_PLANE_DEF_COLOR : CUT_PLANE_ERR_COLOR; if (m_mode == size_t(CutMode::cutTongueAndGroove)) cp_clr.a(cp_clr.a() - 0.1f); m_plane.model.set_color(cp_clr); const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; shader->set_uniform("view_model_matrix", view_model_matrix); m_plane.model.render(); glsafe(::glEnable(GL_CULL_FACE)); glsafe(::glDisable(GL_BLEND)); shader->stop_using(); } static double get_half_size(double size) { return std::max(size * 0.35, 0.05); } static double get_dragging_half_size(double size) { return get_half_size(size) * 1.25; } void GLGizmoCut3D::render_model(GLModel& model, const ColorRGBA& color, Transform3d view_model_matrix) { GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light"); if (shader) { shader->start_using(); shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("emission_factor", 0.2f); shader->set_uniform("projection_matrix", wxGetApp().plater()->get_camera().get_projection_matrix()); model.set_color(color); model.render(); shader->stop_using(); } } void GLGizmoCut3D::render_line(GLModel& line_model, const ColorRGBA& color, Transform3d view_model_matrix, float width) { GLShaderProgram* shader = wxGetApp().get_shader("flat"); if (shader) { shader->start_using(); shader->set_uniform("view_model_matrix", view_model_matrix); shader->set_uniform("projection_matrix", wxGetApp().plater()->get_camera().get_projection_matrix()); shader->set_uniform("width", width); line_model.set_color(color); line_model.render(); shader->stop_using(); } } void GLGizmoCut3D::render_rotation_snapping(GrabberID axis, const ColorRGBA& color) { GLShaderProgram* line_shader = wxGetApp().get_shader("flat"); if (!line_shader) return; const Camera& camera = wxGetApp().plater()->get_camera(); Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(m_plane_center) * m_start_dragging_m; if (axis == X) view_model_matrix = view_model_matrix * rotation_transform(0.5 * PI * Vec3d::UnitY()) * rotation_transform(-PI * Vec3d::UnitZ()); else if (axis == Y) view_model_matrix = view_model_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitZ()) * rotation_transform(-0.5 * PI * Vec3d::UnitY()); else view_model_matrix = view_model_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitZ()); line_shader->start_using(); line_shader->set_uniform("projection_matrix", camera.get_projection_matrix()); line_shader->set_uniform("view_model_matrix", view_model_matrix); line_shader->set_uniform("width", 0.25f); m_circle.render(); m_scale.render(); m_snap_radii.render(); m_reference_radius.render(); if (m_dragging) { line_shader->set_uniform("width", 1.5f); m_angle_arc.set_color(color); m_angle_arc.render(); } line_shader->stop_using(); } void GLGizmoCut3D::render_grabber_connection(const ColorRGBA& color, Transform3d view_matrix, double line_len_koef/* = 1.0*/) { const Transform3d line_view_matrix = view_matrix * scale_transform(Vec3d(1.0, 1.0, line_len_koef * m_grabber_connection_len)); render_line(m_grabber_connection, color, line_view_matrix, 0.2f); }; void GLGizmoCut3D::render_cut_plane_grabbers() { glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); ColorRGBA color = ColorRGBA::GRAY(); const Transform3d view_matrix = wxGetApp().plater()->get_camera().get_view_matrix() * translation_transform(m_plane_center) * m_rotation_m; const double mean_size = get_grabber_mean_size(m_bounding_box); double size; const bool no_xy_dragging = m_dragging && m_hover_id == CutPlane; if (!no_xy_dragging && m_hover_id != CutPlaneZRotation && m_hover_id != CutPlaneXMove && m_hover_id != CutPlaneYMove) { render_grabber_connection(GRABBER_COLOR, view_matrix); // render sphere grabber size = m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size); color = m_hover_id == Y ? complementary(ColorRGBA::GREEN()) : m_hover_id == X ? complementary(ColorRGBA::RED()) : m_hover_id == Z ? GRABBER_COLOR : ColorRGBA::GRAY(); render_model(m_sphere.model, color, view_matrix * translation_transform(m_grabber_connection_len * Vec3d::UnitZ()) * scale_transform(size)); } const bool no_xy_grabber_hovered = !m_dragging && (m_hover_id < 0 || m_hover_id == CutPlane); // render X grabber if (no_xy_grabber_hovered || m_hover_id == X) { size = m_dragging && m_hover_id == X ? get_dragging_half_size(mean_size) : get_half_size(mean_size); const Vec3d cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); color = m_hover_id == X ? complementary(ColorRGBA::RED()) : ColorRGBA::RED(); if (m_hover_id == X) { render_grabber_connection(color, view_matrix); render_rotation_snapping(X, color); } Vec3d offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); } // render Y grabber if (no_xy_grabber_hovered || m_hover_id == Y) { size = m_dragging && m_hover_id == Y ? get_dragging_half_size(mean_size) : get_half_size(mean_size); const Vec3d cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); color = m_hover_id == Y ? complementary(ColorRGBA::GREEN()) : ColorRGBA::GREEN(); if (m_hover_id == Y) { render_grabber_connection(color, view_matrix); render_rotation_snapping(Y, color); } Vec3d offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); } if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { // render CutPlaneZRotation grabber if (no_xy_grabber_hovered || m_hover_id == CutPlaneZRotation) { size = 0.75 * (m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size)); color = ColorRGBA::BLUE(); const ColorRGBA cp_color = m_hover_id == CutPlaneZRotation ? color : m_plane.model.get_color(); const double grabber_shift = -1.75 * m_grabber_connection_len; render_model(m_sphere.model, cp_color, view_matrix * translation_transform(grabber_shift * Vec3d::UnitY()) * scale_transform(size)); if (m_hover_id == CutPlaneZRotation) { const Vec3d cone_scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); render_rotation_snapping(CutPlaneZRotation, color); render_grabber_connection(GRABBER_COLOR, view_matrix * rotation_transform(0.5 * PI * Vec3d::UnitX()), 1.75); Vec3d offset = Vec3d(1.25 * size, grabber_shift, 0.0); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); offset = Vec3d(-1.25 * size, grabber_shift, 0.0); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); } } const double xy_connection_len = 0.75 * m_grabber_connection_len; // render CutPlaneXMove grabber if (no_xy_grabber_hovered || m_hover_id == CutPlaneXMove) { size = (m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size)); color = m_hover_id == CutPlaneXMove ? ColorRGBA::RED() : m_plane.model.get_color(); render_grabber_connection(GRABBER_COLOR, view_matrix * rotation_transform(0.5 * PI * Vec3d::UnitY()), 0.75); Vec3d offset = xy_connection_len * Vec3d::UnitX() - 0.5 * size * Vec3d::Ones(); render_model(m_cube.model, color, view_matrix * translation_transform(offset) * scale_transform(size)); const Vec3d cone_scale = Vec3d(0.5 * size, 0.5 * size, 1.8 * size); offset = (size + xy_connection_len) * Vec3d::UnitX(); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); } // render CutPlaneYMove grabber if (m_groove.angle > 0.0f && (no_xy_grabber_hovered || m_hover_id == CutPlaneYMove)) { size = (m_dragging ? get_dragging_half_size(mean_size) : get_half_size(mean_size)); color = m_hover_id == CutPlaneYMove ? ColorRGBA::GREEN() : m_plane.model.get_color(); render_grabber_connection(GRABBER_COLOR, view_matrix * rotation_transform(-0.5 * PI * Vec3d::UnitX()), 0.75); Vec3d offset = xy_connection_len * Vec3d::UnitY() - 0.5 * size * Vec3d::Ones(); render_model(m_cube.model, color, view_matrix * translation_transform(offset) * scale_transform(size)); const Vec3d cone_scale = Vec3d(0.5 * size, 0.5 * size, 1.8 * size); offset = (size + xy_connection_len) * Vec3d::UnitY(); render_model(m_cone.model, color, view_matrix * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); } } } void GLGizmoCut3D::render_cut_line() { if (!cut_line_processing() || m_line_end.isApprox(Vec3d::Zero())) return; glsafe(::glEnable(GL_DEPTH_TEST)); glsafe(::glClear(GL_DEPTH_BUFFER_BIT)); m_cut_line.reset(); m_cut_line.init_from(its_make_line((Vec3f)m_line_beg.cast(), (Vec3f)m_line_end.cast())); render_line(m_cut_line, GRABBER_COLOR, wxGetApp().plater()->get_camera().get_view_matrix(), 0.25f); } bool GLGizmoCut3D::on_init() { m_grabbers.emplace_back(); m_shortcut_key = WXK_CONTROL_C; // initiate info shortcuts const wxString ctrl = GUI::shortkey_ctrl_prefix(); const wxString alt = GUI::shortkey_alt_prefix(); const wxString shift = "Shift+"; m_shortcuts.push_back(std::make_pair(_L("Left click"), _L("Add connector"))); m_shortcuts.push_back(std::make_pair(_L("Right click"), _L("Remove connector"))); m_shortcuts.push_back(std::make_pair(_L("Drag"), _L("Move connector"))); m_shortcuts.push_back(std::make_pair(shift + _L("Left click"), _L("Add connector to selection"))); m_shortcuts.push_back(std::make_pair(alt + _L("Left click"), _L("Remove connector from selection"))); m_shortcuts.push_back(std::make_pair(ctrl + "A", _L("Select all connectors"))); return true; } void GLGizmoCut3D::on_load(cereal::BinaryInputArchive& ar) { size_t mode; float groove_depth; float groove_width; float groove_flaps_angle; float groove_angle; float groove_depth_tolerance; float groove_width_tolerance; ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, mode, m_connectors_editing, m_ar_plane_center, m_rotation_m, groove_depth, groove_width, groove_flaps_angle, groove_angle, groove_depth_tolerance, groove_width_tolerance); m_start_dragging_m = m_rotation_m; m_transformed_bounding_box = transformed_bounding_box(m_ar_plane_center, m_rotation_m); set_center_pos(m_ar_plane_center); if (m_mode != mode) switch_to_mode(mode); else if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { if (!is_approx(m_groove.depth , groove_depth) || !is_approx(m_groove.width , groove_width) || !is_approx(m_groove.flaps_angle , groove_flaps_angle) || !is_approx(m_groove.angle , groove_angle) || !is_approx(m_groove.depth_tolerance, groove_depth_tolerance) || !is_approx(m_groove.width_tolerance, groove_width_tolerance) ) { m_groove.depth = groove_depth; m_groove.width = groove_width; m_groove.flaps_angle = groove_flaps_angle; m_groove.angle = groove_angle; m_groove.depth_tolerance= groove_depth_tolerance; m_groove.width_tolerance= groove_width_tolerance; update_plane_model(); } reset_cut_by_contours(); } m_parent.request_extra_frame(); } void GLGizmoCut3D::on_save(cereal::BinaryOutputArchive& ar) const { ar( m_keep_upper, m_keep_lower, m_rotate_lower, m_rotate_upper, m_hide_cut_plane, m_mode, m_connectors_editing, m_ar_plane_center, m_start_dragging_m, m_groove.depth, m_groove.width, m_groove.flaps_angle, m_groove.angle, m_groove.depth_tolerance, m_groove.width_tolerance); } std::string GLGizmoCut3D::on_get_name() const { return _u8L("Cut"); } void GLGizmoCut3D::apply_color_clip_plane_colors() { if (CutMode(m_mode) == CutMode::cutTongueAndGroove) m_parent.set_color_clip_plane_colors({ CUT_PLANE_DEF_COLOR , CUT_PLANE_DEF_COLOR }); else m_parent.set_color_clip_plane_colors({ UPPER_PART_COLOR , LOWER_PART_COLOR }); } void GLGizmoCut3D::on_set_state() { if (m_state == On) { m_parent.set_use_color_clip_plane(true); update_bb(); m_connectors_editing = !m_selected.empty(); m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotation_m); // initiate archived values m_ar_plane_center = m_plane_center; m_start_dragging_m = m_rotation_m; reset_cut_by_contours(); m_parent.request_extra_frame(); } else { if (auto oc = m_c->object_clipper()) { oc->set_behavior(true, true, 0.); oc->release(); } m_selected.clear(); m_parent.set_use_color_clip_plane(false); //m_c->selection_info()->set_use_shift(false); // Make sure that the part selection data are released when the gizmo is closed. // The CallAfter is needed because in perform_cut, the gizmo is closed BEFORE // the cut is performed (because of undo/redo snapshots), so the data would // be deleted prematurely. if (m_part_selection.valid()) wxGetApp().CallAfter([this]() { m_part_selection = PartSelection(); }); } } void GLGizmoCut3D::on_register_raycasters_for_picking() { // assert(m_raycasters.empty()); if (!m_raycasters.empty()) on_unregister_raycasters_for_picking(); // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account m_parent.set_raycaster_gizmos_on_top(true); init_picking_models(); if (m_connectors_editing) { if (CommonGizmosDataObjects::SelectionInfo* si = m_c->selection_info()) { const CutConnectors& connectors = si->model_object()->cut_connectors; for (int i = 0; i < int(connectors.size()); ++i) m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, i + m_connectors_group_id, *(m_shapes[connectors[i].attribs]).mesh_raycaster, Transform3d::Identity())); } } else if (!cut_line_processing()) { m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, X, *m_cone.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, X, *m_cone.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Y, *m_cone.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Y, *m_cone.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, Z, *m_sphere.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::FallbackGizmo, CutPlane, *m_plane.mesh_raycaster, Transform3d::Identity())); if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneZRotation, *m_sphere.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneZRotation, *m_cone.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneZRotation, *m_cone.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneXMove, *m_cube.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneXMove, *m_cone.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneYMove, *m_cube.mesh_raycaster, Transform3d::Identity())); m_raycasters.emplace_back(m_parent.add_raycaster_for_picking(SceneRaycaster::EType::Gizmo, CutPlaneYMove, *m_cone.mesh_raycaster, Transform3d::Identity())); } } update_raycasters_for_picking_transform(); } void GLGizmoCut3D::on_unregister_raycasters_for_picking() { m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::Gizmo); m_parent.remove_raycasters_for_picking(SceneRaycaster::EType::FallbackGizmo); m_raycasters.clear(); // the gizmo grabbers are rendered on top of the scene, so the raytraced picker should take it into account m_parent.set_raycaster_gizmos_on_top(false); } void GLGizmoCut3D::update_raycasters_for_picking() { on_unregister_raycasters_for_picking(); on_register_raycasters_for_picking(); } void GLGizmoCut3D::set_volumes_picking_state(bool state) { std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); if (raycasters != nullptr) { const Selection& selection = m_parent.get_selection(); const Selection::IndicesList ids = selection.get_volume_idxs(); for (unsigned int id : ids) { const GLVolume* v = selection.get_volume(id); auto it = std::find_if(raycasters->begin(), raycasters->end(), [v](std::shared_ptr item) { return item->get_raycaster() == v->mesh_raycaster.get(); }); if (it != raycasters->end()) (*it)->set_active(state); } } } void GLGizmoCut3D::update_raycasters_for_picking_transform() { if (m_connectors_editing) { CommonGizmosDataObjects::SelectionInfo* si = m_c->selection_info(); if (!si) return; const ModelObject* mo = si->model_object(); const CutConnectors& connectors = mo->cut_connectors; if (connectors.empty()) return; auto inst_id = m_c->selection_info()->get_active_instance(); if (inst_id < 0) return; const Vec3d& instance_offset = mo->instances[inst_id]->get_offset(); const double sla_shift = double(m_c->selection_info()->get_sla_shift()); const bool looking_forward = is_looking_forward(); for (size_t i = 0; i < connectors.size(); ++i) { const CutConnector& connector = connectors[i]; float height = connector.height; // recalculate connector position to world position Vec3d pos = connector.pos + instance_offset; if (connector.attribs.type == CutConnectorType::Dowel && connector.attribs.style == CutConnectorStyle::Prism) { height = 0.05f; if (!looking_forward) pos += 0.05 * m_clp_normal; } pos[Z] += sla_shift; const Transform3d scale_trafo = scale_transform(Vec3f(connector.radius, connector.radius, height).cast()); m_raycasters[i]->set_transform(translation_transform(pos) * m_rotation_m * scale_trafo); } } else if (!cut_line_processing()){ const Transform3d trafo = translation_transform(m_plane_center) * m_rotation_m; const BoundingBoxf3 box = m_bounding_box; const double size = get_half_size(get_grabber_mean_size(box)); Vec3d scale = Vec3d(0.75 * size, 0.75 * size, 1.8 * size); int id = 0; Vec3d offset = Vec3d(0.0, 1.25 * size, m_grabber_connection_len); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(scale)); offset = Vec3d(0.0, -1.25 * size, m_grabber_connection_len); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitX()) * scale_transform(scale)); offset = Vec3d(1.25 * size, 0.0, m_grabber_connection_len); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); offset = Vec3d(-1.25 * size, 0.0, m_grabber_connection_len); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); m_raycasters[id++]->set_transform(trafo * translation_transform(m_grabber_connection_len * Vec3d::UnitZ()) * scale_transform(size)); m_raycasters[id++]->set_transform(trafo); if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { double grabber_y_shift = -1.75 * m_grabber_connection_len; m_raycasters[id++]->set_transform(trafo * translation_transform(grabber_y_shift * Vec3d::UnitY()) * scale_transform(size)); offset = Vec3d(1.25 * size, grabber_y_shift, 0.0); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); offset = Vec3d(-1.25 * size, grabber_y_shift, 0.0); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitY()) * scale_transform(scale)); const double xy_connection_len = 0.75 * m_grabber_connection_len; const Vec3d cone_scale = Vec3d(0.5 * size, 0.5 * size, 1.8 * size); offset = xy_connection_len * Vec3d::UnitX() - 0.5 * size * Vec3d::Ones(); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * scale_transform(size)); offset = (size + xy_connection_len) * Vec3d::UnitX(); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(0.5 * PI * Vec3d::UnitY()) * scale_transform(cone_scale)); if (m_groove.angle > 0.0f) { offset = xy_connection_len * Vec3d::UnitY() - 0.5 * size * Vec3d::Ones(); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * scale_transform(size)); offset = (size + xy_connection_len) * Vec3d::UnitY(); m_raycasters[id++]->set_transform(trafo * translation_transform(offset) * rotation_transform(-0.5 * PI * Vec3d::UnitX()) * scale_transform(cone_scale)); } else { // discard transformation for CutPlaneYMove grabbers m_raycasters[id++]->set_transform(Transform3d::Identity()); m_raycasters[id++]->set_transform(Transform3d::Identity()); } } } } void GLGizmoCut3D::update_plane_model() { m_plane.reset(); on_unregister_raycasters_for_picking(); init_picking_models(); } void GLGizmoCut3D::on_set_hover_id() { } bool GLGizmoCut3D::on_is_activable() const { const Selection& selection = m_parent.get_selection(); const int object_idx = selection.get_object_idx(); if (object_idx < 0 || selection.is_wipe_tower()) return false; if (const ModelObject* mo = wxGetApp().plater()->model().objects[object_idx]; mo->is_cut() && mo->volumes.size() == 1) { const ModelVolume* volume = mo->volumes[0]; if (volume->is_cut_connector() && volume->cut_info.connector_type == CutConnectorType::Dowel) return false; } // This is assumed in GLCanvas3D::do_rotate, do not change this // without updating that function too. return selection.is_single_full_instance() && !m_parent.is_layers_editing_enabled(); } bool GLGizmoCut3D::on_is_selectable() const { return wxGetApp().get_mode() != comSimple; } Vec3d GLGizmoCut3D::mouse_position_in_local_plane(GrabberID axis, const Linef3& mouse_ray) const { double half_pi = 0.5 * PI; Transform3d m = Transform3d::Identity(); switch (axis) { case X: { m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); m.rotate(Eigen::AngleAxisd(-half_pi, Vec3d::UnitY())); break; } case Y: { m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitY())); m.rotate(Eigen::AngleAxisd(half_pi, Vec3d::UnitZ())); break; } case Z: default: { // no rotation applied break; } } m = m * m_start_dragging_m.inverse(); m.translate(-m_plane_center); return transform(mouse_ray, m).intersect_plane(0.0); } void GLGizmoCut3D::dragging_grabber_move(const GLGizmoBase::UpdateData &data) { Vec3d starting_drag_position; if (m_hover_id == Z) starting_drag_position = translation_transform(m_plane_center) * m_rotation_m * (m_grabber_connection_len * Vec3d::UnitZ()); else starting_drag_position = m_cut_plane_start_move_pos; double projection = 0.0; Vec3d starting_vec = m_rotation_m * (m_hover_id == CutPlaneXMove ? Vec3d::UnitX() : m_hover_id == CutPlaneYMove ? Vec3d::UnitY() : Vec3d::UnitZ()); if (starting_vec.norm() != 0.0) { const Vec3d mouse_dir = data.mouse_ray.unit_vector(); // finds the intersection of the mouse ray with the plane parallel to the camera viewport and passing through the starting position // use ray-plane intersection see i.e. https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection algebraic form // in our case plane normal and ray direction are the same (orthogonal view) // when moving to perspective camera the negative z unit axis of the camera needs to be transformed in world space and used as plane normal const Vec3d inters = data.mouse_ray.a + (starting_drag_position - data.mouse_ray.a).dot(mouse_dir) * mouse_dir; // vector from the starting position to the found intersection const Vec3d inters_vec = inters - starting_drag_position; starting_vec.normalize(); // finds projection of the vector along the staring direction projection = inters_vec.dot(starting_vec); } if (wxGetKeyState(WXK_SHIFT)) projection = m_snap_step * std::round(projection / m_snap_step); const Vec3d shift = starting_vec * projection; if (shift != Vec3d::Zero()) reset_cut_by_contours(); // move cut plane center set_center(m_plane_center + shift, true); m_was_cut_plane_dragged = true; } void GLGizmoCut3D::dragging_grabber_rotation(const GLGizmoBase::UpdateData &data) { const Vec2d mouse_pos = to_2d(mouse_position_in_local_plane((GrabberID)m_hover_id, data.mouse_ray)); const Vec2d orig_dir = Vec2d::UnitX(); const Vec2d new_dir = mouse_pos.normalized(); const double two_pi = 2.0 * PI; double theta = ::acos(std::clamp(new_dir.dot(orig_dir), -1.0, 1.0)); if (cross2(orig_dir, new_dir) < 0.0) theta = two_pi - theta; const double len = mouse_pos.norm(); // snap to coarse snap region if (m_snap_coarse_in_radius <= len && len <= m_snap_coarse_out_radius) { const double step = two_pi / double(SnapRegionsCount); theta = step * std::round(theta / step); } // snap to fine snap region (scale) else if (m_snap_fine_in_radius <= len && len <= m_snap_fine_out_radius) { const double step = two_pi / double(ScaleStepsCount); theta = step * std::round(theta / step); } if (is_approx(theta, two_pi)) theta = 0.0; if (m_hover_id != Y) theta += 0.5 * PI; if (!is_approx(theta, 0.0)) reset_cut_by_contours(); Vec3d rotation = Vec3d::Zero(); rotation[m_hover_id == CutPlaneZRotation ? Z : m_hover_id] = theta; const Transform3d rotation_tmp = m_start_dragging_m * rotation_transform(rotation); const bool update_tbb = !m_rotation_m.rotation().isApprox(rotation_tmp.rotation()); m_rotation_m = rotation_tmp; if (update_tbb) m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotation_m); m_angle = theta; while (m_angle > two_pi) m_angle -= two_pi; if (m_angle < 0.0) m_angle += two_pi; update_clipper(); } void GLGizmoCut3D::dragging_connector(const GLGizmoBase::UpdateData &data) { CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; Vec3d pos; Vec3d pos_world; if (unproject_on_cut_plane(data.mouse_pos.cast(), pos, pos_world)) { connectors[m_hover_id - m_connectors_group_id].pos = pos; update_raycasters_for_picking_transform(); } } void GLGizmoCut3D::on_dragging(const UpdateData& data) { if (m_hover_id < 0) return; if (m_hover_id == Z || m_hover_id == CutPlane || m_hover_id == CutPlaneXMove || m_hover_id == CutPlaneYMove) dragging_grabber_move(data); else if (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation) dragging_grabber_rotation(data); else if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) dragging_connector(data); check_and_update_connectors_state(); if (CutMode(m_mode) == CutMode::cutTongueAndGroove) reset_cut_by_contours(); } void GLGizmoCut3D::on_start_dragging() { m_angle = 0.0; if (m_hover_id >= m_connectors_group_id && m_connector_mode == CutConnectorMode::Manual) Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move connector"), UndoRedo::SnapshotType::GizmoAction); if (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation) m_start_dragging_m = m_rotation_m; } void GLGizmoCut3D::on_stop_dragging() { if (m_hover_id == X || m_hover_id == Y || m_hover_id == CutPlaneZRotation) { m_angle_arc.reset(); m_angle = 0.0; Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Rotate cut plane"), UndoRedo::SnapshotType::GizmoAction); m_start_dragging_m = m_rotation_m; } else if (m_hover_id == Z || m_hover_id == CutPlane || m_hover_id == CutPlaneXMove|| m_hover_id == CutPlaneYMove) { if (m_was_cut_plane_dragged) Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Move cut plane"), UndoRedo::SnapshotType::GizmoAction); m_ar_plane_center = m_plane_center; } if (CutMode(m_mode) == CutMode::cutTongueAndGroove) reset_cut_by_contours(); //check_and_update_connectors_state(); } void GLGizmoCut3D::set_center_pos(const Vec3d& center_pos, bool update_tbb /*=false*/) { BoundingBoxf3 tbb = m_transformed_bounding_box; if (update_tbb) { Vec3d normal = m_rotation_m.inverse() * Vec3d(m_plane_center - center_pos); tbb.translate(normal.z() * Vec3d::UnitZ()); } bool can_set_center_pos = false; { double limit_val = /*CutMode(m_mode) == CutMode::cutTongueAndGroove ? 0.5 * double(m_groove.depth) : */0.5; if (tbb.max.z() > -limit_val && tbb.min.z() < limit_val) can_set_center_pos = true; else { const double old_dist = (m_bb_center - m_plane_center).norm(); const double new_dist = (m_bb_center - center_pos).norm(); // check if forcing is reasonable if (new_dist < old_dist) can_set_center_pos = true; } } if (can_set_center_pos) { m_transformed_bounding_box = tbb; m_plane_center = center_pos; m_center_offset = m_plane_center - m_bb_center; } } BoundingBoxf3 GLGizmoCut3D::bounding_box() const { BoundingBoxf3 ret; const Selection& selection = m_parent.get_selection(); const Selection::IndicesList& idxs = selection.get_volume_idxs(); for (unsigned int i : idxs) { const GLVolume* volume = selection.get_volume(i); // respect just to the solid parts for FFF and ignore pad and supports for SLA if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) ret.merge(volume->transformed_convex_hull_bounding_box()); } return ret; } BoundingBoxf3 GLGizmoCut3D::transformed_bounding_box(const Vec3d& plane_center, const Transform3d& rotation_m/* = Transform3d::Identity()*/) const { const Selection& selection = m_parent.get_selection(); const auto first_volume = selection.get_first_volume(); Vec3d instance_offset = first_volume->get_instance_offset(); instance_offset[Z] += first_volume->get_sla_shift_z(); const auto cut_matrix = Transform3d::Identity() * rotation_m.inverse() * translation_transform(instance_offset - plane_center); const Selection::IndicesList& idxs = selection.get_volume_idxs(); BoundingBoxf3 ret; for (unsigned int i : idxs) { const GLVolume* volume = selection.get_volume(i); // respect just to the solid parts for FFF and ignore pad and supports for SLA if (!volume->is_modifier && !volume->is_sla_pad() && !volume->is_sla_support()) { const auto instance_matrix = volume->get_instance_transformation().get_matrix(true); auto volume_trafo = instance_matrix * volume->get_volume_transformation().get_matrix(); ret.merge(volume->transformed_convex_hull_bounding_box(cut_matrix * volume_trafo)); } } return ret; } void GLGizmoCut3D::update_bb() { const BoundingBoxf3 box = bounding_box(); if (!box.defined) return; if (!m_max_pos.isApprox(box.max) || !m_min_pos.isApprox(box.min)) { m_bounding_box = box; // check, if mode is set to Planar, when object has a connectors if (const int object_idx = m_parent.get_selection().get_object_idx(); object_idx >= 0 && !wxGetApp().plater()->model().objects[object_idx]->cut_connectors.empty()) m_mode = size_t(CutMode::cutPlanar); invalidate_cut_plane(); reset_cut_by_contours(); apply_color_clip_plane_colors(); m_max_pos = box.max; m_min_pos = box.min; m_bb_center = box.center(); m_transformed_bounding_box = transformed_bounding_box(m_bb_center); if (box.contains(m_center_offset)) set_center_pos(m_bb_center + m_center_offset); else set_center_pos(m_bb_center); m_contour_width = CutMode(m_mode) == CutMode::cutTongueAndGroove ? 0.f : 0.4f; m_radius = box.radius(); m_grabber_connection_len = 0.5 * m_radius;// std::min(0.75 * m_radius, 35.0); m_grabber_radius = m_grabber_connection_len * 0.85; m_snap_coarse_in_radius = m_grabber_radius / 3.0; m_snap_coarse_out_radius = m_snap_coarse_in_radius * 2.; m_snap_fine_in_radius = m_grabber_connection_len * 0.85; m_snap_fine_out_radius = m_grabber_connection_len * 1.15; // input params for cut with tongue and groove m_groove.depth = m_groove.depth_init = std::max(1.f , 0.5f * float(get_grabber_mean_size(m_bounding_box))); m_groove.width = m_groove.width_init = 4.0f * m_groove.depth; m_groove.flaps_angle = m_groove.flaps_angle_init = float(PI) / 3.f; m_groove.angle = m_groove.angle_init = 0.f; m_plane.reset(); m_cone.reset(); m_sphere.reset(); m_cube.reset(); m_grabber_connection.reset(); m_circle.reset(); m_scale.reset(); m_snap_radii.reset(); m_reference_radius.reset(); on_unregister_raycasters_for_picking(); clear_selection(); if (CommonGizmosDataObjects::SelectionInfo* selection = m_c->selection_info(); selection && selection->model_object()) m_selected.resize(selection->model_object()->cut_connectors.size(), false); } } void GLGizmoCut3D::init_picking_models() { if (!m_cone.model.is_initialized()) { indexed_triangle_set its = its_make_cone(1.0, 1.0, PI / 12.0); m_cone.model.init_from(its); m_cone.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); } if (!m_sphere.model.is_initialized()) { indexed_triangle_set its = its_make_sphere(1.0, PI / 12.0); m_sphere.model.init_from(its); m_sphere.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); } if (!m_cube.model.is_initialized()) { indexed_triangle_set its = its_make_cube(1., 1., 1.); m_cube.model.init_from(its); m_cube.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); } if (!m_plane.model.is_initialized() && !m_hide_cut_plane && !m_connectors_editing) { const double cp_width = 0.02 * get_grabber_mean_size(m_bounding_box); indexed_triangle_set its = m_mode == size_t(CutMode::cutTongueAndGroove) ? its_make_groove_plane() : its_make_frustum_dowel((double)m_cut_plane_radius_koef * m_radius, cp_width, m_cut_plane_as_circle ? 180 : 4); m_plane.model.init_from(its); m_plane.mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); } if (m_shapes.empty()) init_connector_shapes(); } void GLGizmoCut3D::init_rendering_items() { if (!m_grabber_connection.is_initialized()) m_grabber_connection.init_from(its_make_line(Vec3f::Zero(), Vec3f::UnitZ())); if (!m_circle.is_initialized()) init_from_circle(m_circle, m_grabber_radius); if (!m_scale.is_initialized()) init_from_scale(m_scale, m_grabber_radius); if (!m_snap_radii.is_initialized()) init_from_snap_radii(m_snap_radii, m_grabber_radius); if (!m_reference_radius.is_initialized()) { m_reference_radius.init_from(its_make_line(Vec3f::Zero(), m_grabber_connection_len * Vec3f::UnitX())); m_reference_radius.set_color(ColorRGBA::WHITE()); } if (!m_angle_arc.is_initialized() || m_angle != 0.0) init_from_angle_arc(m_angle_arc, m_angle, m_grabber_connection_len); } void GLGizmoCut3D::render_clipper_cut() { if (! m_connectors_editing) ::glDisable(GL_DEPTH_TEST); GLboolean cull_face = GL_FALSE; ::glGetBooleanv(GL_CULL_FACE, &cull_face); ::glDisable(GL_CULL_FACE); m_c->object_clipper()->render_cut(m_part_selection.get_ignored_contours_ptr()); if (cull_face) ::glEnable(GL_CULL_FACE); if (! m_connectors_editing) ::glEnable(GL_DEPTH_TEST); } void GLGizmoCut3D::PartSelection::add_object(const ModelObject* object) { m_model = Model(); m_model.add_object(*object); const double sla_shift_z = wxGetApp().plater()->canvas3D()->get_selection().get_first_volume()->get_sla_shift_z(); if (!is_approx(sla_shift_z, 0.)) { Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); inst_offset[Z] += sla_shift_z; model_object()->instances[m_instance_idx]->set_offset(inst_offset); } } GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* mo, const Transform3d& cut_matrix, int instance_idx_in, const Vec3d& center, const Vec3d& normal, const CommonGizmosDataObjects::ObjectClipper& oc) : m_instance_idx(instance_idx_in) { Cut cut(mo, instance_idx_in, cut_matrix); add_object(cut.perform_with_plane().front()); const ModelVolumePtrs& volumes = model_object()->volumes; // split to parts for (int id = int(volumes.size())-1; id >= 0; id--) if (volumes[id]->is_splittable()) volumes[id]->split(1); m_parts.clear(); for (const ModelVolume* volume : volumes) { assert(volume != nullptr); m_parts.emplace_back(Part{GLModel(), MeshRaycaster(volume->mesh()), true, !volume->is_model_part()}); m_parts.back().glmodel.set_color({ 0.f, 0.f, 1.f, 1.f }); m_parts.back().glmodel.init_from(volume->mesh()); // Now check whether this part is below or above the plane. Transform3d tr = (model_object()->instances[m_instance_idx]->get_matrix() * volume->get_matrix()).inverse(); Vec3f pos = (tr * center).cast(); Vec3f norm = (tr.linear().inverse().transpose() * normal).cast(); for (const Vec3f& v : volume->mesh().its.vertices) { double p = (v - pos).dot(norm); if (std::abs(p) > EPSILON) { m_parts.back().selected = p > 0.; break; } } } // Now go through the contours and create a map from contours to parts. m_contour_points.clear(); m_contour_to_parts.clear(); m_debug_pts = std::vector>(m_parts.size(), std::vector()); if (std::vector pts = oc.point_per_contour();! pts.empty()) { m_contour_to_parts.resize(pts.size()); for (size_t pt_idx=0; pt_idxinstances[m_instance_idx]->get_offset()) * translation_transform(model_object()->volumes[part_id]->get_offset())).inverse(); for (double d : {-1., 1.}) { const Vec3d dir_mesh = d * tr.linear().inverse().transpose() * normal; const Vec3d src = tr * (m_contour_points[pt_idx] + d*0.01 * normal); AABBMesh::hit_result hit = aabb.query_ray_hit(src, dir_mesh); m_debug_pts[part_id].emplace_back(src); if (hit.is_inside()) { // This part belongs to this point. if (d == 1.) m_contour_to_parts[pt_idx].first.emplace_back(part_id); else m_contour_to_parts[pt_idx].second.emplace_back(part_id); } } } } } m_valid = true; } // In CutMode::cutTongueAndGroove we use PartSelection just for rendering GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* object, int instance_idx_in) : m_instance_idx (instance_idx_in) { add_object(object); m_parts.clear(); for (const ModelVolume* volume : object->volumes) { assert(volume != nullptr); m_parts.emplace_back(Part{ GLModel(), MeshRaycaster(volume->mesh()), true, !volume->is_model_part() }); m_parts.back().glmodel.init_from(volume->mesh()); // Now check whether this part is below or above the plane. m_parts.back().selected = volume->is_from_upper(); } m_valid = true; } void GLGizmoCut3D::PartSelection::render(const Vec3d* normal, GLModel& sphere_model) { if (! valid()) return; const Camera& camera = wxGetApp().plater()->get_camera(); if (GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light")) { shader->start_using(); shader->set_uniform("projection_matrix", camera.get_projection_matrix()); shader->set_uniform("emission_factor", 0.f); // FIXME: Cache the transforms. const Vec3d inst_offset = model_object()->instances[m_instance_idx]->get_offset(); const Transform3d view_inst_matrix= camera.get_view_matrix() * translation_transform(inst_offset); const bool is_looking_forward = normal && camera.get_dir_forward().dot(*normal) < 0.05; for (size_t id=0; idset_uniform("view_model_matrix", view_inst_matrix * model_object()->volumes[id]->get_matrix()); if (m_parts[id].is_modifier) { glsafe(::glEnable(GL_BLEND)); glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)); } m_parts[id].glmodel.set_color(m_parts[id].is_modifier ? MODIFIER_COLOR : (m_parts[id].selected ? UPPER_PART_COLOR : LOWER_PART_COLOR)); m_parts[id].glmodel.render(); if (m_parts[id].is_modifier) glsafe(::glDisable(GL_BLEND)); } shader->stop_using(); } // { // Debugging render: // static int idx = -1; // ImGui::Begin("DEBUG"); // for (int i=0; i= m_parts.size()) // idx = -1; // ImGui::End(); // ::glDisable(GL_DEPTH_TEST); // if (valid()) { // for (size_t i=0; i GLGizmoCut3D::PartSelection::get_cut_parts() { std::vector parts; for (const auto& part : m_parts) parts.push_back({part.selected, part.is_modifier}); return parts; } void GLGizmoCut3D::PartSelection::toggle_selection(const Vec2d& mouse_pos) { // FIXME: Cache the transforms. const Camera& camera = wxGetApp().plater()->get_camera(); const Vec3d& camera_pos = camera.get_position(); Vec3f pos; Vec3f normal; std::vector> hits_id_and_sqdist; for (size_t id=0; idvolumes[id]->get_offset(); Transform3d tr = translation_transform(model_object()->instances[m_instance_idx]->get_offset()) * translation_transform(model_object()->volumes[id]->get_offset()); if (m_parts[id].raycaster.unproject_on_mesh(mouse_pos, tr, camera, pos, normal)) { hits_id_and_sqdist.emplace_back(id, (camera_pos - tr*(pos.cast())).squaredNorm()); } } if (! hits_id_and_sqdist.empty()) { size_t id = std::min_element(hits_id_and_sqdist.begin(), hits_id_and_sqdist.end(), [](const std::pair& a, const std::pair& b) { return a.second < b.second; })->first; m_parts[id].selected = ! m_parts[id].selected; // And now recalculate the contours which should be ignored. m_ignored_contours.clear(); size_t cont_id = 0; for (const auto& [parts_above, parts_below] : m_contour_to_parts) { for (size_t upper : parts_above) { bool upper_sel = m_parts[upper].selected; if (std::find_if(parts_below.begin(), parts_below.end(), [this, &upper_sel](const size_t& i) { return m_parts[i].selected == upper_sel; }) != parts_below.end()) { m_ignored_contours.emplace_back(cont_id); break; } } ++cont_id; } } } void GLGizmoCut3D::PartSelection::turn_over_selection() { for (Part& part : m_parts) part.selected = !part.selected; } void GLGizmoCut3D::on_render() { if (m_state == On) { // This gizmo is showing the object elevated. Tell the common // SelectionInfo object to lie about the actual shift. //m_c->selection_info()->set_use_shift(true); } // check objects visibility toggle_model_objects_visibility(); update_clipper(); init_picking_models(); init_rendering_items(); render_connectors(); if (!m_connectors_editing) m_part_selection.render(nullptr, m_sphere.model); else m_part_selection.render(&m_cut_normal, m_sphere.model); render_clipper_cut(); if (!m_hide_cut_plane && !m_connectors_editing) { render_cut_plane(); render_cut_plane_grabbers(); } render_cut_line(); m_selection_rectangle.render(m_parent); } void GLGizmoCut3D::render_debug_input_window(float x) { return; m_imgui->begin(wxString("DEBUG")); m_imgui->end(); /* static bool hide_clipped = false; static bool fill_cut = false; static float contour_width = 0.4f; m_imgui->checkbox(_L("Hide cut plane and grabbers"), m_hide_cut_plane); if (m_imgui->checkbox("hide_clipped", hide_clipped) && !hide_clipped) m_clp_normal = m_c->object_clipper()->get_clipping_plane()->get_normal(); m_imgui->checkbox("fill_cut", fill_cut); m_imgui->slider_float("contour_width", &contour_width, 0.f, 3.f); if (auto oc = m_c->object_clipper()) oc->set_behavior(hide_clipped || m_connectors_editing, fill_cut || m_connectors_editing, double(contour_width)); */ ImGui::PushItemWidth(0.5f * m_label_width); if (auto oc = m_c->object_clipper(); oc && m_imgui->slider_float("contour_width", &m_contour_width, 0.f, 3.f)) oc->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); ImGui::Separator(); if (m_imgui->checkbox(("Render cut plane as disc"), m_cut_plane_as_circle)) m_plane.reset(); ImGui::PushItemWidth(0.5f * m_label_width); if (m_imgui->slider_float("cut_plane_radius_koef", &m_cut_plane_radius_koef, 1.f, 2.f)) m_plane.reset(); m_imgui->end(); } void GLGizmoCut3D::adjust_window_position(float x, float y, float bottom_limit) { static float last_y = 0.0f; static float last_h = 0.0f; const float win_h = ImGui::GetWindowHeight(); y = std::min(y, bottom_limit - win_h); ImGui::SetWindowPos(ImVec2(x, y), ImGuiCond_Always); if (!is_approx(last_h, win_h) || !is_approx(last_y, y)) { // ask canvas for another frame to render the window in the correct position m_imgui->set_requires_extra_frame(); if (!is_approx(last_h, win_h)) last_h = win_h; if (!is_approx(last_y, y)) last_y = y; } } void GLGizmoCut3D::unselect_all_connectors() { std::fill(m_selected.begin(), m_selected.end(), false); m_selected_count = 0; validate_connector_settings(); } void GLGizmoCut3D::select_all_connectors() { std::fill(m_selected.begin(), m_selected.end(), true); m_selected_count = int(m_selected.size()); } void GLGizmoCut3D::render_shortcuts() { if (m_imgui->button("? " + (m_show_shortcuts ? wxString(ImGui::CollapseBtn) : wxString(ImGui::ExpandBtn)))) m_show_shortcuts = !m_show_shortcuts; if (m_shortcut_label_width < 0.f) { for (const auto& shortcut : m_shortcuts) { const float width = m_imgui->calc_text_size(shortcut.first).x; if (m_shortcut_label_width < width) m_shortcut_label_width = width; } m_shortcut_label_width += +m_imgui->scaled(1.f); } if (m_show_shortcuts) for (const auto&shortcut : m_shortcuts ){ m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, shortcut.first); ImGui::SameLine(m_shortcut_label_width); m_imgui->text(shortcut.second); } } void GLGizmoCut3D::apply_selected_connectors(std::function apply_fn) { for (size_t idx = 0; idx < m_selected.size(); idx++) if (m_selected[idx]) apply_fn(idx); check_and_update_connectors_state(); update_raycasters_for_picking_transform(); } void GLGizmoCut3D::render_connectors_input_window(CutConnectors &connectors) { // add shortcuts panel render_shortcuts(); // Connectors section ImGui::Separator(); // WIP : Auto : Need to implement // m_imgui->text(_L("Mode")); // render_connect_mode_radio_button(CutConnectorMode::Auto); // render_connect_mode_radio_button(CutConnectorMode::Manual); ImGui::AlignTextToFramePadding(); m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, m_labels_map["Connectors"]); m_imgui->disabled_begin(connectors.empty()); ImGui::SameLine(m_label_width); const std::string act_name = _u8L("Remove connectors"); if (render_reset_button("connectors", act_name)) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), act_name, UndoRedo::SnapshotType::GizmoAction); reset_connectors(); } m_imgui->disabled_end(); render_flip_plane_button(m_connectors_editing && connectors.empty()); m_imgui->text(m_labels_map["Type"]); bool type_changed = render_connect_type_radio_button(CutConnectorType::Plug); type_changed |= render_connect_type_radio_button(CutConnectorType::Dowel); type_changed |= render_connect_type_radio_button(CutConnectorType::Snap); if (type_changed) apply_selected_connectors([this, &connectors] (size_t idx) { connectors[idx].attribs.type = CutConnectorType(m_connector_type); }); m_imgui->disabled_begin(m_connector_type != CutConnectorType::Plug); if (type_changed && m_connector_type == CutConnectorType::Dowel) { m_connector_style = int(CutConnectorStyle::Prism); apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); } if (render_combo(m_labels_map["Style"], m_connector_styles, m_connector_style)) apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.style = CutConnectorStyle(m_connector_style); }); m_imgui->disabled_end(); m_imgui->disabled_begin(m_connector_type == CutConnectorType::Snap); if (type_changed && m_connector_type == CutConnectorType::Snap) { m_connector_shape_id = int(CutConnectorShape::Circle); apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); } if (render_combo(m_labels_map["Shape"], m_connector_shapes, m_connector_shape_id)) apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].attribs.shape = CutConnectorShape(m_connector_shape_id); }); m_imgui->disabled_end(); const float depth_min_value = m_connector_type == CutConnectorType::Snap ? m_connector_size : -0.1f; if (render_slider_double_input(m_labels_map["Depth"], m_connector_depth_ratio, m_connector_depth_ratio_tolerance, depth_min_value)) apply_selected_connectors([this, &connectors](size_t idx) { if (m_connector_depth_ratio > 0) connectors[idx].height = m_connector_depth_ratio; if (m_connector_depth_ratio_tolerance >= 0) connectors[idx].height_tolerance = m_connector_depth_ratio_tolerance; }); if (render_slider_double_input(m_labels_map["Size"], m_connector_size, m_connector_size_tolerance)) apply_selected_connectors([this, &connectors](size_t idx) { if (m_connector_size > 0) connectors[idx].radius = 0.5f * m_connector_size; if (m_connector_size_tolerance >= 0) connectors[idx].radius_tolerance = 0.5f * m_connector_size_tolerance; }); if (render_angle_input(m_labels_map["Rotation"], m_connector_angle, 0.f, 0.f, 180.f)) apply_selected_connectors([this, &connectors](size_t idx) { connectors[idx].z_angle = m_connector_angle; }); if (m_connector_type == CutConnectorType::Snap) { render_snap_specific_input(_u8L("Bulge"), _L("Bulge proportion related to radius"), m_snap_bulge_proportion, 0.15f, 5.f, 100.f * m_snap_space_proportion); render_snap_specific_input(_u8L("Space"), _L("Space proportion related to radius"), m_snap_space_proportion, 0.3f, 10.f, 50.f); } ImGui::Separator(); if (m_imgui->button(_L("Confirm connectors"))) { unselect_all_connectors(); set_connectors_editing(false); } ImGui::SameLine(m_label_width + 1.15f * m_control_width); if (m_imgui->button(_L("Cancel"))) { reset_connectors(); set_connectors_editing(false); } } void GLGizmoCut3D::render_build_size() { double koef = m_imperial_units ? GizmoObjectManipulation::mm_to_in : 1.0; wxString unit_str = " " + (m_imperial_units ? _L("in") : _L("mm")); Vec3d tbb_sz = m_transformed_bounding_box.size(); wxString size = "X: " + double_to_string(tbb_sz.x() * koef, 2) + unit_str + ", Y: " + double_to_string(tbb_sz.y() * koef, 2) + unit_str + ", Z: " + double_to_string(tbb_sz.z() * koef, 2) + unit_str; ImGui::AlignTextToFramePadding(); m_imgui->text(_L("Build Volume")); ImGui::SameLine(); m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, size); } void GLGizmoCut3D::reset_cut_plane() { m_angle_arc.reset(); m_transformed_bounding_box = transformed_bounding_box(m_bb_center); set_center(m_bb_center); m_start_dragging_m = m_rotation_m = Transform3d::Identity(); m_ar_plane_center = m_plane_center; reset_cut_by_contours(); m_parent.request_extra_frame(); } void GLGizmoCut3D::invalidate_cut_plane() { m_rotation_m = Transform3d::Identity(); m_plane_center = Vec3d::Zero(); m_min_pos = Vec3d::Zero(); m_max_pos = Vec3d::Zero(); m_bb_center = Vec3d::Zero(); m_center_offset = Vec3d::Zero(); } void GLGizmoCut3D::set_connectors_editing(bool connectors_editing) { if (m_connectors_editing == connectors_editing) return; m_connectors_editing = connectors_editing; update_raycasters_for_picking(); m_c->object_clipper()->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); m_parent.request_extra_frame(); } void GLGizmoCut3D::flip_cut_plane() { m_rotation_m = m_rotation_m * rotation_transform(PI * Vec3d::UnitX()); m_transformed_bounding_box = transformed_bounding_box(m_plane_center, m_rotation_m); Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Flip cut plane"), UndoRedo::SnapshotType::GizmoAction); m_start_dragging_m = m_rotation_m; update_clipper(); m_part_selection.turn_over_selection(); if (CutMode(m_mode) == CutMode::cutTongueAndGroove) reset_cut_by_contours(); } void GLGizmoCut3D::reset_cut_by_contours() { m_part_selection = PartSelection(); if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { if (m_dragging || m_groove_editing || !has_valid_groove()) return; process_contours(); } else toggle_model_objects_visibility(); } void GLGizmoCut3D::process_contours() { const Selection& selection = m_parent.get_selection(); const ModelObjectPtrs& model_objects = selection.get_model()->objects; const int instance_idx = selection.get_instance_idx(); if (instance_idx < 0) return; const int object_idx = selection.get_object_idx(); wxBusyCursor wait; if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { if (has_valid_groove()) { Cut cut(model_objects[object_idx], instance_idx, get_cut_matrix(selection)); const ModelObjectPtrs& new_objects = cut.perform_with_groove(m_groove, m_rotation_m, true); if (!new_objects.empty()) m_part_selection = PartSelection(new_objects.front(), instance_idx); } } else { reset_cut_by_contours(); m_part_selection = PartSelection(model_objects[object_idx], get_cut_matrix(selection), instance_idx, m_plane_center, m_cut_normal, *m_c->object_clipper()); } toggle_model_objects_visibility(); } void GLGizmoCut3D::render_flip_plane_button(bool disable_pred /*=false*/) { ImGui::SameLine(); if (m_hover_id == CutPlane) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetColorU32(ImGuiCol_ButtonHovered)); m_imgui->disabled_begin(disable_pred); if (m_imgui->button(_L("Flip cut plane"))) flip_cut_plane(); m_imgui->disabled_end(); if (m_hover_id == CutPlane) ImGui::PopStyleColor(); } void GLGizmoCut3D::add_vertical_scaled_interval(float interval) { ImGui::GetCurrentWindow()->DC.CursorPos.y += m_imgui->scaled(interval); } void GLGizmoCut3D::add_horizontal_scaled_interval(float interval) { ImGui::GetCurrentWindow()->DC.CursorPos.x += m_imgui->scaled(interval); } void GLGizmoCut3D::add_horizontal_shift(float shift) { ImGui::GetCurrentWindow()->DC.CursorPos.x += shift; } void GLGizmoCut3D::render_color_marker(float size, const ImU32& color) { ImGui::SameLine(); const float radius = 0.5f * size; ImVec2 pos = ImGui::GetCurrentWindow()->DC.CursorPos; pos.x += size; pos.y += 1.25f * radius; ImGui::GetCurrentWindow()->DrawList->AddNgonFilled(pos, radius, color, 6); m_imgui->text(" "); } void GLGizmoCut3D::render_groove_float_input(const std::string& label, float& in_val, const float& init_val, float& in_tolerance) { bool is_changed{false}; float val = in_val; float tolerance = in_tolerance; if (render_slider_double_input(label, val, tolerance, -0.1f, std::min(0.3f*in_val, 1.5f))) { if (m_imgui->get_last_slider_status().can_take_snapshot) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", _u8L("Groove change"), label), UndoRedo::SnapshotType::GizmoAction); m_imgui->get_last_slider_status().invalidate_snapshot(); m_groove_editing = true; } in_val = val; in_tolerance = tolerance; is_changed = true; } ImGui::SameLine(); m_imgui->disabled_begin(is_approx(in_val, init_val) && is_approx(in_tolerance, 0.1f)); const std::string act_name = _u8L("Reset"); if (render_reset_button(("##groove_" + label + act_name).c_str(), act_name)) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", act_name, label), UndoRedo::SnapshotType::GizmoAction); in_val = init_val; in_tolerance = 0.1f; is_changed = true; } m_imgui->disabled_end(); if (is_changed) { update_plane_model(); reset_cut_by_contours(); } if (m_is_slider_editing_done) { m_groove_editing = false; reset_cut_by_contours(); } } bool GLGizmoCut3D::render_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) { bool is_changed{ false }; m_imgui->text(label); ImGui::SameLine(m_label_width); ImGui::PushItemWidth(m_control_width * 0.7f); float val = rad2deg(in_val); const float old_val = val; const std::string format = "%.0f " + _u8L("°"); m_imgui->slider_float(("##angle_" + label).c_str(), &val, min_val, max_val, format.c_str(), 1.f, true, from_u8(label)); m_is_slider_editing_done |= m_imgui->get_last_slider_status().deactivated_after_edit; if (!is_approx(old_val, val)) { if (m_imgui->get_last_slider_status().can_take_snapshot) { // TRN: This is an entry in the Undo/Redo stack. The whole line will be 'Edited: (name of whatever was edited)'. Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", _L("Edited"), label), UndoRedo::SnapshotType::GizmoAction); m_imgui->get_last_slider_status().invalidate_snapshot(); if (m_mode == size_t(CutMode::cutTongueAndGroove)) m_groove_editing = true; } in_val = deg2rad(val); is_changed = true; } ImGui::SameLine(); m_imgui->disabled_begin(is_approx(in_val, init_val)); const std::string act_name = _u8L("Reset"); if (render_reset_button(("##angle_" + label + act_name).c_str(), act_name)) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), GUI::format("%1%: %2%", act_name, label), UndoRedo::SnapshotType::GizmoAction); in_val = init_val; is_changed = true; } m_imgui->disabled_end(); return is_changed; } void GLGizmoCut3D::render_groove_angle_input(const std::string& label, float& in_val, const float& init_val, float min_val, float max_val) { if (render_angle_input(label, in_val, init_val, min_val, max_val)) { update_plane_model(); reset_cut_by_contours(); } if (m_is_slider_editing_done) { m_groove_editing = false; reset_cut_by_contours(); } } void GLGizmoCut3D::render_snap_specific_input(const std::string& label, const wxString& tooltip, float& in_val, const float& init_val, const float min_val, const float max_val) { m_imgui->text(label); ImGui::SameLine(m_label_width); ImGui::PushItemWidth(m_control_width * 0.7f); bool is_changed = false; const std::string format = "%.0f %%"; float val = in_val * 100.f; if (m_imgui->slider_float(("##snap_" + label).c_str(), &val, min_val, max_val, format.c_str(), 1.f, true, tooltip)) { in_val = val * 0.01f; is_changed = true; } ImGui::SameLine(); m_imgui->disabled_begin(is_approx(in_val, init_val)); const std::string act_name = _u8L("Reset"); if (render_reset_button(("##snap_" + label + act_name).c_str(), act_name)) { in_val = init_val; is_changed = true; } m_imgui->disabled_end(); if (is_changed) { update_connector_shape(); update_raycasters_for_picking(); } } void GLGizmoCut3D::render_cut_plane_input_window(CutConnectors &connectors) { // if (m_mode == size_t(CutMode::cutPlanar)) { CutMode mode = CutMode(m_mode); if (mode == CutMode::cutPlanar || mode == CutMode::cutTongueAndGroove) { ImGui::AlignTextToFramePadding(); m_imgui->text(wxString(ImGui::InfoMarkerSmall)); ImGui::SameLine(); m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, get_wraped_wxString(_L("Hold SHIFT key to draw a cut line"), 40)); ImGui::Separator(); const bool has_connectors = !connectors.empty(); m_imgui->disabled_begin(has_connectors); if (render_cut_mode_combo()) mode = CutMode(m_mode); m_imgui->disabled_end(); render_build_size(); ImGui::AlignTextToFramePadding(); m_imgui->text(_L("Cut position") + ": "); ImGui::SameLine(); render_move_center_input(Z); ImGui::SameLine(); const bool is_cut_plane_init = m_rotation_m.isApprox(Transform3d::Identity()) && m_bb_center.isApprox(m_plane_center); m_imgui->disabled_begin(is_cut_plane_init); std::string act_name = _u8L("Reset cutting plane"); if (render_reset_button("cut_plane", into_u8(act_name))) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), act_name, UndoRedo::SnapshotType::GizmoAction); reset_cut_plane(); } m_imgui->disabled_end(); // render_flip_plane_button(); if (mode == CutMode::cutPlanar) { add_vertical_scaled_interval(0.75f); m_imgui->disabled_begin(!m_keep_upper || !m_keep_lower || m_keep_as_parts || (m_part_selection.valid() && m_part_selection.is_one_object())); if (m_imgui->button(has_connectors ? _L("Edit connectors") : _L("Add connectors"))) set_connectors_editing(true); m_imgui->disabled_end(); ImGui::SameLine(1.5f * m_control_width); m_imgui->disabled_begin(is_cut_plane_init && !has_connectors); act_name = _u8L("Reset cut"); if (m_imgui->button(act_name, _u8L("Reset cutting plane and remove connectors"))) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), act_name, UndoRedo::SnapshotType::GizmoAction); reset_cut_plane(); reset_connectors(); } m_imgui->disabled_end(); } else if (mode == CutMode::cutTongueAndGroove) { m_is_slider_editing_done = false; ImGui::Separator(); m_imgui->text_colored(ImGuiWrapper::COL_ORANGE_LIGHT, m_labels_map["Groove"] + ": "); render_groove_float_input(m_labels_map["Depth"], m_groove.depth, m_groove.depth_init, m_groove.depth_tolerance); render_groove_float_input(m_labels_map["Width"], m_groove.width, m_groove.width_init, m_groove.width_tolerance); render_groove_angle_input(m_labels_map["Flap Angle"], m_groove.flaps_angle, m_groove.flaps_angle_init, 30.f, 120.f); render_groove_angle_input(m_labels_map["Groove Angle"], m_groove.angle, m_groove.angle_init, 0.f, 15.f); } ImGui::Separator(); // render "After Cut" section ImVec2 label_size; for (const auto& item : m_part_orientation_names) { const ImVec2 text_size = m_imgui->calc_text_size(item.second); if (label_size.x < text_size.x) label_size.x = text_size.x; if (label_size.y < text_size.y) label_size.y = text_size.y; } const float h_shift = label_size.x + m_imgui->scaled(3.f); const float marker_size = label_size.y; auto render_part_name = [this, marker_size, has_connectors](const wxString& name, bool& keep_part, const ImU32& color) { bool keep = true; add_horizontal_shift(m_imgui->scaled(1.2f)); m_imgui->checkbox((m_keep_as_parts ? _L("Part") : _L("Object")) + " " + name, has_connectors ? keep : keep_part); render_color_marker(marker_size, color); }; auto render_part_actions = [this, h_shift] (const wxString& suffix, const bool& keep_part, bool& place_on_cut_part, bool& rotate_part) { float shift = m_imgui->scaled(1.2f); if (suffix == "##lower") shift += h_shift; m_imgui->disabled_begin(!keep_part || m_keep_as_parts); add_horizontal_shift(shift); if (m_imgui->radio_button(m_part_orientation_names.at("none") + suffix, !place_on_cut_part && !rotate_part)) { rotate_part = false; place_on_cut_part = false; } add_horizontal_shift(shift); if (m_imgui->radio_button(m_part_orientation_names.at("on_cut") + suffix, place_on_cut_part)) { place_on_cut_part = !place_on_cut_part; rotate_part = false; } add_horizontal_shift(shift); if (m_imgui->radio_button(m_part_orientation_names.at("flip") + suffix, rotate_part)) { rotate_part = !rotate_part; place_on_cut_part = false; } m_imgui->disabled_end(); }; m_imgui->text(_L("Cut result") + ": "); add_vertical_scaled_interval(0.5f); m_imgui->disabled_begin(has_connectors || m_keep_as_parts); render_part_name("A", m_keep_upper, m_imgui->to_ImU32(UPPER_PART_COLOR)); ImGui::SameLine(h_shift + ImGui::GetCurrentWindow()->WindowPadding.x); render_part_name("B", m_keep_lower, m_imgui->to_ImU32(LOWER_PART_COLOR)); m_imgui->disabled_end(); add_vertical_scaled_interval(0.5f); const ImVec2 pos = ImGui::GetCurrentWindow()->DC.CursorPos; render_part_actions("##upper", m_keep_upper, m_place_on_cut_upper, m_rotate_upper); ImGui::GetCurrentWindow()->DC.CursorPos = pos; render_part_actions("##lower", m_keep_lower, m_place_on_cut_lower, m_rotate_lower); add_vertical_scaled_interval(0.75f); m_imgui->disabled_begin(has_connectors || m_part_selection.valid() || mode == CutMode::cutTongueAndGroove); m_imgui->text(_L("Cut into") + ":"); if (m_part_selection.valid()) m_keep_as_parts = false; add_horizontal_scaled_interval(1.2f); // TRN CutGizmo: RadioButton Cut into ... if (m_imgui->radio_button(_L("Objects"), !m_keep_as_parts)) m_keep_as_parts = false; ImGui::SameLine(); // TRN CutGizmo: RadioButton Cut into ... if (m_imgui->radio_button(_L("Parts"), m_keep_as_parts)) m_keep_as_parts = true; if (m_keep_as_parts) { m_keep_upper = m_keep_lower = true; m_place_on_cut_upper = m_place_on_cut_lower = false; m_rotate_upper = m_rotate_lower = false; } m_imgui->disabled_end(); } ImGui::Separator(); m_imgui->disabled_begin(!can_perform_cut()); if(m_imgui->button(_L("Perform cut"))) perform_cut(m_parent.get_selection()); m_imgui->disabled_end(); } void GLGizmoCut3D::validate_connector_settings() { if (m_connector_depth_ratio < 0.f) m_connector_depth_ratio = 3.f; if (m_connector_depth_ratio_tolerance < 0.f) m_connector_depth_ratio_tolerance = 0.1f; if (m_connector_size < 0.f) m_connector_size = 2.5f; if (m_connector_size_tolerance < 0.f) m_connector_size_tolerance = 0.f; if (m_connector_angle < 0.f || m_connector_angle > float(PI) ) m_connector_angle = 0.f; if (m_connector_type == CutConnectorType::Undef) m_connector_type = CutConnectorType::Plug; if (m_connector_style == int(CutConnectorStyle::Undef)) m_connector_style = int(CutConnectorStyle::Prism); if (m_connector_shape_id == int(CutConnectorShape::Undef)) m_connector_shape_id = int(CutConnectorShape::Circle); } void GLGizmoCut3D::init_input_window_data(CutConnectors &connectors) { m_imperial_units = wxGetApp().app_config->get_bool("use_inches"); m_control_width = m_imgui->get_font_size() * 9.f; if (m_connectors_editing && m_selected_count > 0) { float depth_ratio { UndefFloat }; float depth_ratio_tolerance { UndefFloat }; float radius { UndefFloat }; float radius_tolerance { UndefFloat }; float angle { UndefFloat }; CutConnectorType type { CutConnectorType::Undef }; CutConnectorStyle style { CutConnectorStyle::Undef }; CutConnectorShape shape { CutConnectorShape::Undef }; bool is_init = false; for (size_t idx = 0; idx < m_selected.size(); idx++) if (m_selected[idx]) { const CutConnector& connector = connectors[idx]; if (!is_init) { depth_ratio = connector.height; depth_ratio_tolerance = connector.height_tolerance; radius = connector.radius; radius_tolerance = connector.radius_tolerance; angle = connector.z_angle; type = connector.attribs.type; style = connector.attribs.style; shape = connector.attribs.shape; if (m_selected_count == 1) break; is_init = true; } else { if (!is_approx(depth_ratio, connector.height)) depth_ratio = UndefFloat; if (!is_approx(depth_ratio_tolerance, connector.height_tolerance)) depth_ratio_tolerance = UndefFloat; if (!is_approx(radius,connector.radius)) radius = UndefFloat; if (!is_approx(radius_tolerance, connector.radius_tolerance)) radius_tolerance = UndefFloat; if (!is_approx(angle, connector.z_angle)) angle = UndefFloat; if (type != connector.attribs.type) type = CutConnectorType::Undef; if (style != connector.attribs.style) style = CutConnectorStyle::Undef; if (shape != connector.attribs.shape) shape = CutConnectorShape::Undef; } } m_connector_depth_ratio = depth_ratio; m_connector_depth_ratio_tolerance = depth_ratio_tolerance; m_connector_size = 2.f * radius; m_connector_size_tolerance = 2.f * radius_tolerance; m_connector_type = type; m_connector_angle = angle; m_connector_style = int(style); m_connector_shape_id = int(shape); } if (m_label_width == 0.f) { for (const auto& item : m_labels_map) { const float width = m_imgui->calc_text_size(item.second).x; if (m_label_width < width) m_label_width = width; } m_label_width += m_imgui->scaled(1.f); } } void GLGizmoCut3D::render_input_window_warning() const { if (! m_invalid_connectors_idxs.empty()) { wxString out = /*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _L("Invalid connectors detected") + ":"; if (m_info_stats.outside_cut_contour > size_t(0)) out += "\n - " + format_wxstr(_L_PLURAL("%1$d connector is out of cut contour", "%1$d connectors are out of cut contour", m_info_stats.outside_cut_contour), m_info_stats.outside_cut_contour); if (m_info_stats.outside_bb > size_t(0)) out += "\n - " + format_wxstr(_L_PLURAL("%1$d connector is out of object", "%1$d connectors are out of object", m_info_stats.outside_bb), m_info_stats.outside_bb); if (m_info_stats.is_overlap) out += "\n - " + _L("Some connectors are overlapped"); m_imgui->text(out); } if (!m_keep_upper && !m_keep_lower) m_imgui->text(/*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _L("Select at least one object to keep after cutting.")); if (!has_valid_contour()) m_imgui->text(/*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _L("Cut plane is placed out of object")); else if (!has_valid_groove()) m_imgui->text(/*wxString(ImGui::WarningMarkerSmall)*/ _L("Warning") + ": " + _L("Cut plane with groove is invalid")); } void GLGizmoCut3D::on_render_input_window(float x, float y, float bottom_limit) { m_imgui->begin(get_name(), ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); // adjust window position to avoid overlap the view toolbar adjust_window_position(x, y, bottom_limit); CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; init_input_window_data(connectors); if (m_connectors_editing) // connectors mode render_connectors_input_window(connectors); else render_cut_plane_input_window(connectors); render_input_window_warning(); m_imgui->end(); if (!m_connectors_editing) // connectors mode render_debug_input_window(x); } bool GLGizmoCut3D::is_outside_of_cut_contour(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos) { // check if connector pos is out of clipping plane if (m_c->object_clipper() && m_c->object_clipper()->is_projection_inside_cut(cur_pos) == -1) { m_info_stats.outside_cut_contour++; return true; } // check if connector bottom contour is out of clipping plane const CutConnector& cur_connector = connectors[idx]; const CutConnectorShape shape = CutConnectorShape(cur_connector.attribs.shape); const int sectorCount = shape == CutConnectorShape::Triangle ? 3 : shape == CutConnectorShape::Square ? 4 : shape == CutConnectorShape::Circle ? 60: // supposably, 60 points are enough for conflict detection shape == CutConnectorShape::Hexagon ? 6 : 1 ; indexed_triangle_set mesh; auto& vertices = mesh.vertices; vertices.reserve(sectorCount + 1); float fa = 2 * PI / sectorCount; auto vec = Eigen::Vector2f(0, cur_connector.radius); for (float angle = 0; angle < 2.f * PI; angle += fa) { Vec2f p = Eigen::Rotation2Df(angle) * vec; vertices.emplace_back(Vec3f(p(0), p(1), 0.f)); } its_transform(mesh, translation_transform(cur_pos) * m_rotation_m); for (const Vec3f& vertex : vertices) { if (m_c->object_clipper()) { int contour_idx = m_c->object_clipper()->is_projection_inside_cut(vertex.cast()); bool is_invalid = (contour_idx == -1); if (m_part_selection.valid() && ! is_invalid) { assert(contour_idx >= 0); const std::vector& ignored = *(m_part_selection.get_ignored_contours_ptr()); is_invalid = (std::find(ignored.begin(), ignored.end(), size_t(contour_idx)) != ignored.end()); } if (is_invalid) { m_info_stats.outside_cut_contour++; return true; } } } return false; } bool GLGizmoCut3D::is_conflict_for_connector(size_t idx, const CutConnectors& connectors, const Vec3d cur_pos) { if (is_outside_of_cut_contour(idx, connectors, cur_pos)) return true; const CutConnector& cur_connector = connectors[idx]; const Transform3d matrix = translation_transform(cur_pos) * m_rotation_m * scale_transform(Vec3f(cur_connector.radius, cur_connector.radius, cur_connector.height).cast()); const BoundingBoxf3 cur_tbb = m_shapes[cur_connector.attribs].model.get_bounding_box().transformed(matrix); // check if connector's bounding box is inside the object's bounding box if (!m_bounding_box.contains(cur_tbb)) { m_info_stats.outside_bb++; return true; } // check if connectors are overlapping for (size_t i = 0; i < connectors.size(); ++i) { if (i == idx) continue; const CutConnector& connector = connectors[i]; if ((connector.pos - cur_connector.pos).norm() < double(connector.radius + cur_connector.radius)) { m_info_stats.is_overlap = true; return true; } } return false; } void GLGizmoCut3D::check_and_update_connectors_state() { m_info_stats.invalidate(); m_invalid_connectors_idxs.clear(); if (CutMode(m_mode) != CutMode::cutPlanar) return; const ModelObject* mo = m_c->selection_info()->model_object(); auto inst_id = m_c->selection_info()->get_active_instance(); if (inst_id < 0) return; const CutConnectors& connectors = mo->cut_connectors; const ModelInstance* mi = mo->instances[inst_id]; const Vec3d& instance_offset = mi->get_offset(); const double sla_shift = double(m_c->selection_info()->get_sla_shift()); for (size_t i = 0; i < connectors.size(); ++i) { const CutConnector& connector = connectors[i]; Vec3d pos = connector.pos + instance_offset + sla_shift * Vec3d::UnitZ(); // recalculate connector position to world position if (is_conflict_for_connector(i, connectors, pos)) m_invalid_connectors_idxs.emplace_back(i); } } void GLGizmoCut3D::toggle_model_objects_visibility() { bool has_active_volume = false; std::vector>* raycasters = m_parent.get_raycasters_for_picking(SceneRaycaster::EType::Volume); for (const std::shared_ptr &raycaster : *raycasters) if (raycaster->is_active()) { has_active_volume = true; break; } if (m_part_selection.valid() && has_active_volume) m_parent.toggle_model_objects_visibility(false); else if (!m_part_selection.valid() && !has_active_volume) { const Selection& selection = m_parent.get_selection(); const ModelObjectPtrs& model_objects = selection.get_model()->objects; m_parent.toggle_model_objects_visibility(true, model_objects[selection.get_object_idx()], selection.get_instance_idx()); } } void GLGizmoCut3D::render_connectors() { ::glEnable(GL_DEPTH_TEST); if (cut_line_processing() || CutMode(m_mode) != CutMode::cutPlanar || m_connector_mode == CutConnectorMode::Auto || !m_c->selection_info()) return; const ModelObject* mo = m_c->selection_info()->model_object(); auto inst_id = m_c->selection_info()->get_active_instance(); if (inst_id < 0) return; const CutConnectors& connectors = mo->cut_connectors; if (connectors.size() != m_selected.size()) { // #ysFIXME clear_selection(); m_selected.resize(connectors.size(), false); } ColorRGBA render_color = CONNECTOR_DEF_COLOR; const ModelInstance* mi = mo->instances[inst_id]; const Vec3d& instance_offset = mi->get_offset(); const double sla_shift = double(m_c->selection_info()->get_sla_shift()); const bool looking_forward = is_looking_forward(); for (size_t i = 0; i < connectors.size(); ++i) { const CutConnector& connector = connectors[i]; float height = connector.height; // recalculate connector position to world position Vec3d pos = connector.pos + instance_offset + sla_shift * Vec3d::UnitZ(); // First decide about the color of the point. assert(std::is_sorted(m_invalid_connectors_idxs.begin(), m_invalid_connectors_idxs.end())); const bool conflict_connector = std::binary_search(m_invalid_connectors_idxs.begin(), m_invalid_connectors_idxs.end(), i); if (conflict_connector) render_color = CONNECTOR_ERR_COLOR; else // default connector color render_color = connector.attribs.type == CutConnectorType::Dowel ? DOWEL_COLOR : PLAG_COLOR; if (!m_connectors_editing) render_color = CONNECTOR_ERR_COLOR; else if (size_t(m_hover_id - m_connectors_group_id) == i) render_color = conflict_connector ? HOVERED_ERR_COLOR : connector.attribs.type == CutConnectorType::Dowel ? HOVERED_DOWEL_COLOR : HOVERED_PLAG_COLOR; else if (m_selected[i]) render_color = connector.attribs.type == CutConnectorType::Dowel ? SELECTED_DOWEL_COLOR : SELECTED_PLAG_COLOR; const Camera& camera = wxGetApp().plater()->get_camera(); if (connector.attribs.type == CutConnectorType::Dowel && connector.attribs.style == CutConnectorStyle::Prism) { if (m_connectors_editing) { height = 0.05f; if (!looking_forward) pos += 0.05 * m_clp_normal; } else { if (looking_forward) pos -= static_cast(height) * m_clp_normal; else pos += static_cast(height) * m_clp_normal; height *= 2; } } else if (!looking_forward) pos += 0.05 * m_clp_normal; const Transform3d view_model_matrix = camera.get_view_matrix() * translation_transform(pos) * m_rotation_m * rotation_transform(-connector.z_angle * Vec3d::UnitZ()) * scale_transform(Vec3f(connector.radius, connector.radius, height).cast()); render_model(m_shapes[connector.attribs].model, render_color, view_model_matrix); } } bool GLGizmoCut3D::can_perform_cut() const { if (! m_invalid_connectors_idxs.empty() || (!m_keep_upper && !m_keep_lower) || m_connectors_editing) return false; if (CutMode(m_mode) == CutMode::cutTongueAndGroove) return has_valid_groove(); if (m_part_selection.valid()) return ! m_part_selection.is_one_object(); return true; } bool GLGizmoCut3D::has_valid_groove() const { if (CutMode(m_mode) != CutMode::cutTongueAndGroove) return true; const float flaps_width = -2.f * m_groove.depth / tan(m_groove.flaps_angle); if (flaps_width > m_groove.width) return false; const Selection& selection = m_parent.get_selection(); const auto&list = selection.get_volume_idxs(); // is more volumes selected? if (list.empty()) return false; const Transform3d cp_matrix = translation_transform(m_plane_center) * m_rotation_m; for (size_t id = 0; id < m_groove_vertices.size(); id += 2) { const Vec3d beg = cp_matrix * m_groove_vertices[id]; const Vec3d end = cp_matrix * m_groove_vertices[id + 1]; bool intersection = false; for (const unsigned int volume_idx : list) { const GLVolume* glvol = selection.get_volume(volume_idx); if (!glvol->is_modifier && glvol->mesh_raycaster->intersects_line(beg, end - beg, glvol->world_matrix())) { intersection = true; break; } } if (!intersection) return false; } return true; } bool GLGizmoCut3D::has_valid_contour() const { const auto clipper = m_c->object_clipper(); return clipper && clipper->has_valid_contour(); } void GLGizmoCut3D::apply_connectors_in_model(ModelObject* mo, int &dowels_count) { if (CutMode(m_mode) == CutMode::cutTongueAndGroove) return; if (m_connector_mode == CutConnectorMode::Manual) { clear_selection(); for (CutConnector&connector : mo->cut_connectors) { connector.rotation_m = m_rotation_m; if (connector.attribs.type == CutConnectorType::Dowel) { if (connector.attribs.style == CutConnectorStyle::Prism) connector.height *= 2; dowels_count ++; } else { // calculate shift of the connector center regarding to the position on the cut plane connector.pos += m_cut_normal * 0.5 * double(connector.height); } } apply_cut_connectors(mo, _u8L("Connector")); } } Transform3d GLGizmoCut3D::get_cut_matrix(const Selection& selection) { const int instance_idx = selection.get_instance_idx(); const int object_idx = selection.get_object_idx(); ModelObject* mo = selection.get_model()->objects[object_idx]; if (!mo) return Transform3d::Identity(); // m_cut_z is the distance from the bed. Subtract possible SLA elevation. const double sla_shift_z = selection.get_first_volume()->get_sla_shift_z(); const Vec3d instance_offset = mo->instances[instance_idx]->get_offset(); Vec3d cut_center_offset = m_plane_center - instance_offset; cut_center_offset[Z] -= sla_shift_z; return translation_transform(cut_center_offset) * m_rotation_m; } void update_object_cut_id(CutObjectBase& cut_id, ModelObjectCutAttributes attributes, const int dowels_count) { // we don't save cut information, if result will not contains all parts of initial object if (!attributes.has(ModelObjectCutAttribute::KeepUpper) || !attributes.has(ModelObjectCutAttribute::KeepLower) || attributes.has(ModelObjectCutAttribute::InvalidateCutInfo)) return; if (cut_id.id().invalid()) cut_id.init(); // increase check sum, if it's needed { int cut_obj_cnt = -1; if (attributes.has(ModelObjectCutAttribute::KeepUpper)) cut_obj_cnt++; if (attributes.has(ModelObjectCutAttribute::KeepLower)) cut_obj_cnt++; if (attributes.has(ModelObjectCutAttribute::CreateDowels)) cut_obj_cnt+= dowels_count; if (cut_obj_cnt > 0) cut_id.increase_check_sum(size_t(cut_obj_cnt)); } } void synchronize_model_after_cut(Model& model, const CutObjectBase& cut_id) { for (ModelObject* obj : model.objects) if (obj->is_cut() && obj->cut_id.has_same_id(cut_id) && !obj->cut_id.is_equal(cut_id)) obj->cut_id.copy(cut_id); } void GLGizmoCut3D::perform_cut(const Selection& selection) { if (!can_perform_cut()) return; const int instance_idx = selection.get_instance_idx(); const int object_idx = selection.get_object_idx(); wxCHECK_RET(instance_idx >= 0 && object_idx >= 0, "GLGizmoCut: Invalid object selection"); Plater* plater = wxGetApp().plater(); ModelObject* mo = plater->model().objects[object_idx]; if (!mo) return; // deactivate CutGizmo and than perform a cut m_parent.reset_all_gizmos(); // perform cut { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Cut by Plane")); // This shall delete the part selection class and deallocate the memory. ScopeGuard part_selection_killer([this]() { m_part_selection = PartSelection(); }); const bool cut_with_groove = CutMode(m_mode) == CutMode::cutTongueAndGroove; const bool cut_by_contour = !cut_with_groove && m_part_selection.valid(); ModelObject* cut_mo = cut_by_contour ? m_part_selection.model_object() : nullptr; if (cut_mo) cut_mo->cut_connectors = mo->cut_connectors; else cut_mo = mo; int dowels_count = 0; const bool has_connectors = !mo->cut_connectors.empty(); // update connectors pos as offset of its center before cut performing apply_connectors_in_model(cut_mo , dowels_count); wxBusyCursor wait; ModelObjectCutAttributes attributes = only_if(has_connectors ? true : m_keep_upper, ModelObjectCutAttribute::KeepUpper) | only_if(has_connectors ? true : m_keep_lower, ModelObjectCutAttribute::KeepLower) | only_if(has_connectors ? false : m_keep_as_parts, ModelObjectCutAttribute::KeepAsParts) | only_if(m_place_on_cut_upper, ModelObjectCutAttribute::PlaceOnCutUpper) | only_if(m_place_on_cut_lower, ModelObjectCutAttribute::PlaceOnCutLower) | only_if(m_rotate_upper, ModelObjectCutAttribute::FlipUpper) | only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower) | only_if(dowels_count > 0, ModelObjectCutAttribute::CreateDowels) | only_if(!has_connectors && !cut_with_groove && cut_mo->cut_id.id().invalid(), ModelObjectCutAttribute::InvalidateCutInfo); // update cut_id for the cut object in respect to the attributes update_object_cut_id(cut_mo->cut_id, attributes, dowels_count); Cut cut(cut_mo, instance_idx, get_cut_matrix(selection), attributes); const ModelObjectPtrs& new_objects = cut_by_contour ? cut.perform_by_contour(m_part_selection.get_cut_parts(), dowels_count): cut_with_groove ? cut.perform_with_groove(m_groove, m_rotation_m) : cut.perform_with_plane(); // save cut_id to post update synchronization const CutObjectBase cut_id = cut_mo->cut_id; // update cut results on plater and in the model plater->apply_cut_object_to_model(object_idx, new_objects); synchronize_model_after_cut(plater->model(), cut_id); } } // Unprojects the mouse position on the mesh and saves hit point and normal of the facet into pos_and_normal // Return false if no intersection was found, true otherwise. bool GLGizmoCut3D::unproject_on_cut_plane(const Vec2d& mouse_position, Vec3d& pos, Vec3d& pos_world, bool respect_contours/* = true*/) { const float sla_shift = m_c->selection_info()->get_sla_shift(); const ModelObject* mo = m_c->selection_info()->model_object(); const ModelInstance* mi = mo->instances[m_c->selection_info()->get_active_instance()]; const Camera& camera = wxGetApp().plater()->get_camera(); // Calculate intersection with the clipping plane. const ClippingPlane* cp = m_c->object_clipper()->get_clipping_plane(true); Vec3d point; Vec3d direction; Vec3d hit; MeshRaycaster::line_from_mouse_pos(mouse_position, Transform3d::Identity(), camera, point, direction); Vec3d normal = -cp->get_normal().cast(); double den = normal.dot(direction); if (den != 0.) { double t = (-cp->get_offset() - normal.dot(point))/den; hit = (point + t * direction); } else return false; // Now check if the hit is not obscured by a selected part on this side of the plane. // FIXME: This would be better solved by remembering which contours are active. We will // probably need that anyway because there is not other way to find out which contours // to render. If you want to uncomment it, fix it first. It does not work yet. /*for (size_t id = 0; id < m_part_selection.parts.size(); ++id) { if (! m_part_selection.parts[id].selected) { Vec3f pos, normal; const ModelObject* model_object = m_part_selection.model_object; const Vec3d volume_offset = m_part_selection.model_object->volumes[id]->get_offset(); Transform3d tr = model_object->instances[m_part_selection.instance_idx]->get_matrix() * model_object->volumes[id]->get_matrix(); if (m_part_selection.parts[id].raycaster.unproject_on_mesh(mouse_position, tr, camera, pos, normal)) return false; } }*/ if (respect_contours) { // Do not react to clicks outside a contour (or inside a contour that is ignored) int cont_id = m_c->object_clipper()->is_projection_inside_cut(hit); if (cont_id == -1) return false; if (m_part_selection.valid()) { const std::vector& ign = *m_part_selection.get_ignored_contours_ptr(); if (std::find(ign.begin(), ign.end(), cont_id) != ign.end()) return false; } } // recalculate hit to object's local position Vec3d hit_d = hit; hit_d -= mi->get_offset(); hit_d[Z] -= sla_shift; // Return both the point and the facet normal. pos = hit_d; pos_world = hit; return true; } void GLGizmoCut3D::clear_selection() { m_selected.clear(); m_selected_count = 0; } void GLGizmoCut3D::reset_connectors() { m_c->selection_info()->model_object()->cut_connectors.clear(); update_raycasters_for_picking(); clear_selection(); check_and_update_connectors_state(); } void GLGizmoCut3D::init_connector_shapes() { for (const CutConnectorType& type : {CutConnectorType::Dowel, CutConnectorType::Plug, CutConnectorType::Snap}) for (const CutConnectorStyle& style : {CutConnectorStyle::Frustum, CutConnectorStyle::Prism}) { if (type == CutConnectorType::Dowel && style == CutConnectorStyle::Frustum) continue; for (const CutConnectorShape& shape : {CutConnectorShape::Circle, CutConnectorShape::Hexagon, CutConnectorShape::Square, CutConnectorShape::Triangle}) { if (type == CutConnectorType::Snap && shape != CutConnectorShape::Circle) continue; const CutConnectorAttributes attribs = { type, style, shape }; indexed_triangle_set its = get_connector_mesh(attribs); m_shapes[attribs].model.init_from(its); m_shapes[attribs].mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); } } } void GLGizmoCut3D::update_connector_shape() { CutConnectorAttributes attribs = { m_connector_type, CutConnectorStyle(m_connector_style), CutConnectorShape(m_connector_shape_id) }; if (m_connector_type == CutConnectorType::Snap) { indexed_triangle_set its = get_connector_mesh(attribs); m_shapes[attribs].reset(); m_shapes[attribs].model.init_from(its); m_shapes[attribs].mesh_raycaster = std::make_unique(std::make_shared(std::move(its))); //const indexed_triangle_set its = get_connector_mesh(attribs); //m_connector_mesh.clear(); //m_connector_mesh = TriangleMesh(its); } } bool GLGizmoCut3D::cut_line_processing() const { return !m_line_beg.isApprox(Vec3d::Zero()); } void GLGizmoCut3D::discard_cut_line_processing() { m_line_beg = m_line_end = Vec3d::Zero(); } bool GLGizmoCut3D::process_cut_line(SLAGizmoEventType action, const Vec2d& mouse_position) { const Camera& camera = wxGetApp().plater()->get_camera(); Vec3d pt; Vec3d dir; MeshRaycaster::line_from_mouse_pos(mouse_position, Transform3d::Identity(), camera, pt, dir); dir.normalize(); pt += dir; // Move the pt along dir so it is not clipped. if (action == SLAGizmoEventType::LeftDown && !cut_line_processing()) { m_line_beg = pt; m_line_end = pt; on_unregister_raycasters_for_picking(); return true; } if (cut_line_processing()) { if (CutMode(m_mode) == CutMode::cutTongueAndGroove) m_groove_editing = true; reset_cut_by_contours(); m_line_end = pt; if (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp) { Vec3d line_dir = m_line_end - m_line_beg; if (line_dir.norm() < 3.0) return true; Vec3d cross_dir = line_dir.cross(dir).normalized(); Eigen::Quaterniond q; Transform3d m = Transform3d::Identity(); m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(Vec3d::UnitZ(), cross_dir).toRotationMatrix(); const Vec3d new_plane_center = m_bb_center + cross_dir * cross_dir.dot(pt - m_bb_center); // update transformed bb const auto new_tbb = transformed_bounding_box(new_plane_center, m); const GLVolume* first_volume = m_parent.get_selection().get_first_volume(); Vec3d instance_offset = first_volume->get_instance_offset(); instance_offset[Z] += first_volume->get_sla_shift_z(); const Vec3d trans_center_pos = m.inverse() * (new_plane_center - instance_offset) + new_tbb.center(); if (new_tbb.contains(trans_center_pos)) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Cut by line"), UndoRedo::SnapshotType::GizmoAction); m_transformed_bounding_box = new_tbb; set_center(new_plane_center); m_start_dragging_m = m_rotation_m = m; m_ar_plane_center = m_plane_center; } m_angle_arc.reset(); discard_cut_line_processing(); if (CutMode(m_mode) == CutMode::cutTongueAndGroove) { m_groove_editing = false; reset_cut_by_contours(); } } else if (action == SLAGizmoEventType::Moving) this->set_dirty(); return true; } return false; } bool GLGizmoCut3D::add_connector(CutConnectors& connectors, const Vec2d& mouse_position) { if (!m_connectors_editing) return false; Vec3d pos; Vec3d pos_world; if (unproject_on_cut_plane(mouse_position.cast(), pos, pos_world)) { Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Add connector"), UndoRedo::SnapshotType::GizmoAction); unselect_all_connectors(); connectors.emplace_back(pos, m_rotation_m, m_connector_size * 0.5f, m_connector_depth_ratio, m_connector_size_tolerance * 0.5f, m_connector_depth_ratio_tolerance, m_connector_angle, CutConnectorAttributes( CutConnectorType(m_connector_type), CutConnectorStyle(m_connector_style), CutConnectorShape(m_connector_shape_id))); m_selected.push_back(true); m_selected_count = 1; assert(m_selected.size() == connectors.size()); update_raycasters_for_picking(); m_parent.set_as_dirty(); check_and_update_connectors_state(); return true; } return false; } bool GLGizmoCut3D::delete_selected_connectors(CutConnectors& connectors) { if (connectors.empty()) return false; Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Delete connector"), UndoRedo::SnapshotType::GizmoAction); // remove connectors for (int i = int(connectors.size()) - 1; i >= 0; i--) if (m_selected[i]) connectors.erase(connectors.begin() + i); // remove selections m_selected.erase(std::remove_if(m_selected.begin(), m_selected.end(), [](const auto& selected) { return selected; }), m_selected.end()); m_selected_count = 0; assert(m_selected.size() == connectors.size()); update_raycasters_for_picking(); m_parent.set_as_dirty(); check_and_update_connectors_state(); return true; } void GLGizmoCut3D::select_connector(int idx, bool select) { m_selected[idx] = select; if (select) ++m_selected_count; else --m_selected_count; } bool GLGizmoCut3D::is_selection_changed(bool alt_down, bool shift_down) { if (m_hover_id >= m_connectors_group_id) { if (alt_down) select_connector(m_hover_id - m_connectors_group_id, false); else { if (!shift_down) unselect_all_connectors(); select_connector(m_hover_id - m_connectors_group_id, true); } return true; } return false; } void GLGizmoCut3D::process_selection_rectangle(CutConnectors &connectors) { GLSelectionRectangle::EState rectangle_status = m_selection_rectangle.get_state(); ModelObject* mo = m_c->selection_info()->model_object(); int active_inst = m_c->selection_info()->get_active_instance(); // First collect positions of all the points in world coordinates. Transformation trafo = mo->instances[active_inst]->get_transformation(); trafo.set_offset(trafo.get_offset() + double(m_c->selection_info()->get_sla_shift()) * Vec3d::UnitZ()); std::vector points; for (const CutConnector&connector : connectors) points.push_back(connector.pos + trafo.get_offset()); // Now ask the rectangle which of the points are inside. std::vector points_idxs = m_selection_rectangle.contains(points); m_selection_rectangle.stop_dragging(); for (size_t idx : points_idxs) select_connector(int(idx), rectangle_status == GLSelectionRectangle::EState::Select); } bool GLGizmoCut3D::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_position, bool shift_down, bool alt_down, bool control_down) { if (is_dragging() || m_connector_mode == CutConnectorMode::Auto) return false; if ( (m_hover_id < 0 || m_hover_id == CutPlane) && shift_down && ! m_connectors_editing && (action == SLAGizmoEventType::LeftDown || action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::Moving) ) return process_cut_line(action, mouse_position); if (!m_keep_upper || !m_keep_lower) return false; if (!m_connectors_editing) return false; CutConnectors& connectors = m_c->selection_info()->model_object()->cut_connectors; if (action == SLAGizmoEventType::LeftDown) { if (shift_down || alt_down) { // left down with shift - show the selection rectangle: if (m_hover_id == -1) m_selection_rectangle.start_dragging(mouse_position, shift_down ? GLSelectionRectangle::EState::Select : GLSelectionRectangle::EState::Deselect); } else // If there is no selection and no hovering, add new point if (m_hover_id == -1 && !shift_down && !alt_down) if (!add_connector(connectors, mouse_position)) m_ldown_mouse_position = mouse_position; return true; } if (action == SLAGizmoEventType::LeftUp && !m_selection_rectangle.is_dragging()) { if ((m_ldown_mouse_position - mouse_position).norm() < 5.) unselect_all_connectors(); return is_selection_changed(alt_down, shift_down); } // left up with selection rectangle - select points inside the rectangle: if ((action == SLAGizmoEventType::LeftUp || action == SLAGizmoEventType::ShiftUp || action == SLAGizmoEventType::AltUp) && m_selection_rectangle.is_dragging()) { // Is this a selection or deselection rectangle? process_selection_rectangle(connectors); return true; } // dragging the selection rectangle: if (action == SLAGizmoEventType::Dragging) { if (m_selection_rectangle.is_dragging()) { m_selection_rectangle.dragging(mouse_position); return true; } return false; } if (action == SLAGizmoEventType::RightDown && !shift_down) { // If any point is in hover state, this should initiate its move - return control back to GLCanvas: if (m_hover_id < m_connectors_group_id) return false; unselect_all_connectors(); select_connector(m_hover_id - m_connectors_group_id, true); return delete_selected_connectors(connectors); } if (action == SLAGizmoEventType::Delete) return delete_selected_connectors(connectors); if (action == SLAGizmoEventType::SelectAll) { select_all_connectors(); return true; } return false; } CommonGizmosDataID GLGizmoCut3D::on_get_requirements() const { return CommonGizmosDataID( int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::InstancesHider) | int(CommonGizmosDataID::ObjectClipper)); } void GLGizmoCut3D::data_changed(bool is_serializing) { update_bb(); if (auto oc = m_c->object_clipper()) oc->set_behavior(m_connectors_editing, m_connectors_editing, double(m_contour_width)); } indexed_triangle_set GLGizmoCut3D::get_connector_mesh(CutConnectorAttributes connector_attributes) { indexed_triangle_set connector_mesh; int sectorCount{ 1 }; switch (CutConnectorShape(connector_attributes.shape)) { case CutConnectorShape::Triangle: sectorCount = 3; break; case CutConnectorShape::Square: sectorCount = 4; break; case CutConnectorShape::Circle: sectorCount = 360; break; case CutConnectorShape::Hexagon: sectorCount = 6; break; default: break; } if (connector_attributes.type == CutConnectorType::Snap) connector_mesh = its_make_snap(1.0, 1.0, m_snap_space_proportion, m_snap_bulge_proportion); else if (connector_attributes.style == CutConnectorStyle::Prism) connector_mesh = its_make_cylinder(1.0, 1.0, (2 * PI / sectorCount)); else if (connector_attributes.type == CutConnectorType::Plug) connector_mesh = its_make_frustum(1.0, 1.0, (2 * PI / sectorCount)); else connector_mesh = its_make_frustum_dowel(1.0, 1.0, sectorCount); return connector_mesh; } void GLGizmoCut3D::apply_cut_connectors(ModelObject* mo, const std::string& connector_name) { if (mo->cut_connectors.empty()) return; using namespace Geometry; size_t connector_id = mo->cut_id.connectors_cnt(); for (const CutConnector& connector : mo->cut_connectors) { TriangleMesh mesh = TriangleMesh(get_connector_mesh(connector.attribs)); // Mesh will be centered when loading. ModelVolume* new_volume = mo->add_volume(std::move(mesh), ModelVolumeType::NEGATIVE_VOLUME); // Transform the new modifier to be aligned inside the instance new_volume->set_transformation(translation_transform(connector.pos) * connector.rotation_m * rotation_transform(-connector.z_angle * Vec3d::UnitZ()) * scale_transform(Vec3f(connector.radius, connector.radius, connector.height).cast())); new_volume->cut_info = { connector.attribs.type, connector.radius_tolerance, connector.height_tolerance }; new_volume->name = connector_name + "-" + std::to_string(++connector_id); } mo->cut_id.increase_connectors_cnt(mo->cut_connectors.size()); // delete all connectors mo->cut_connectors.clear(); } } // namespace GUI } // namespace Slic3r